From 2fe77b519a9843d228f1fbb1dd6ee25ff99881d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:04:36 +0000 Subject: [PATCH 1/5] Initial plan From 14ee2863ab135ee0511515e1ccb6d214c087f7ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:07:05 +0000 Subject: [PATCH 2/5] Fix SQL injection vulnerability in Neon provider table name Co-authored-by: mfeltscher <1352744+mfeltscher@users.noreply.github.com> --- package-lock.json | 11 ++++++++++ src/cache-tags/provider/neon.ts | 38 +++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index c2e0317..5f18502 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,6 +96,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -898,6 +899,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -1398,6 +1400,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1696,6 +1699,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2224,6 +2228,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2284,6 +2289,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -4376,6 +4382,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5054,6 +5061,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5203,6 +5211,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5268,6 +5277,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -5477,6 +5487,7 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/cache-tags/provider/neon.ts b/src/cache-tags/provider/neon.ts index f745872..00b2cfe 100644 --- a/src/cache-tags/provider/neon.ts +++ b/src/cache-tags/provider/neon.ts @@ -21,16 +21,40 @@ type NeonCacheTagsProviderConfig = { readonly table: string; }; +/** + * Validates and quotes a PostgreSQL identifier (table name, column name, etc.) to prevent SQL injection. + * @param identifier The identifier to validate and quote + * @returns The properly quoted identifier + * @throws Error if the identifier is invalid + */ +function quoteIdentifier(identifier: string): string { + // Validate that the identifier contains only valid characters + // PostgreSQL identifiers can contain letters, digits, underscores, and dollar signs + // They can also contain dots for schema-qualified names + if (!/^[a-zA-Z_][a-zA-Z0-9_$]*(\.[a-zA-Z_][a-zA-Z0-9_$]*)?$/.test(identifier)) { + throw new Error( + `Invalid table name: ${identifier}. Table names must start with a letter or underscore and contain only letters, digits, underscores, and dollar signs.`, + ); + } + + // Quote the identifier using double quotes to prevent SQL injection + // Handle schema-qualified names (e.g., "schema.table") + return identifier + .split('.') + .map((part) => `"${part}"`) + .join('.'); +} + /** * A `CacheTagsProvider` implementation that uses Neon as the storage backend. */ export class NeonCacheTagsProvider implements CacheTagsProvider { private readonly sql; - private readonly table; + private readonly quotedTable; constructor({ connectionUrl, table }: NeonCacheTagsProviderConfig) { this.sql = neon(connectionUrl, { fullResults: true }); - this.table = table; + this.quotedTable = quoteIdentifier(table); } public async storeQueryCacheTags(queryId: string, cacheTags: CacheTag[]) { @@ -41,7 +65,7 @@ export class NeonCacheTagsProvider implements CacheTagsProvider { const tags = cacheTags.flatMap((_, i) => [queryId, cacheTags[i]]); const placeholders = cacheTags.map((_, i) => `($${2 * i + 1}, $${2 * i + 2})`).join(','); - await this.sql.query(`INSERT INTO ${this.table} VALUES ${placeholders} ON CONFLICT DO NOTHING`, tags); + await this.sql.query(`INSERT INTO ${this.quotedTable} VALUES ${placeholders} ON CONFLICT DO NOTHING`, tags); } public async queriesReferencingCacheTags(cacheTags: CacheTag[]): Promise { @@ -52,7 +76,7 @@ export class NeonCacheTagsProvider implements CacheTagsProvider { const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(','); const { rows } = await this.sql.query( - `SELECT DISTINCT query_id FROM ${this.table} WHERE cache_tag IN (${placeholders})`, + `SELECT DISTINCT query_id FROM ${this.quotedTable} WHERE cache_tag IN (${placeholders})`, cacheTags, ); @@ -71,10 +95,12 @@ export class NeonCacheTagsProvider implements CacheTagsProvider { } const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(','); - return (await this.sql.query(`DELETE FROM ${this.table} WHERE cache_tag IN (${placeholders})`, cacheTags)).rowCount ?? 0; + return ( + (await this.sql.query(`DELETE FROM ${this.quotedTable} WHERE cache_tag IN (${placeholders})`, cacheTags)).rowCount ?? 0 + ); } public async truncateCacheTags() { - return (await this.sql.query(`DELETE FROM ${this.table}`)).rowCount ?? 0; + return (await this.sql.query(`DELETE FROM ${this.quotedTable}`)).rowCount ?? 0; } } From 85408489120b27420ddb85823c90c37ef247757b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:08:23 +0000 Subject: [PATCH 3/5] Improve identifier validation and escape double quotes Co-authored-by: mfeltscher <1352744+mfeltscher@users.noreply.github.com> --- src/cache-tags/provider/neon.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/cache-tags/provider/neon.ts b/src/cache-tags/provider/neon.ts index 00b2cfe..30f8e82 100644 --- a/src/cache-tags/provider/neon.ts +++ b/src/cache-tags/provider/neon.ts @@ -31,17 +31,18 @@ function quoteIdentifier(identifier: string): string { // Validate that the identifier contains only valid characters // PostgreSQL identifiers can contain letters, digits, underscores, and dollar signs // They can also contain dots for schema-qualified names - if (!/^[a-zA-Z_][a-zA-Z0-9_$]*(\.[a-zA-Z_][a-zA-Z0-9_$]*)?$/.test(identifier)) { + if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)?$/.test(identifier)) { throw new Error( - `Invalid table name: ${identifier}. Table names must start with a letter or underscore and contain only letters, digits, underscores, and dollar signs.`, + `Invalid table name: ${identifier}. Table names must start with a letter, underscore, or dollar sign and contain only letters, digits, underscores, and dollar signs.`, ); } // Quote the identifier using double quotes to prevent SQL injection // Handle schema-qualified names (e.g., "schema.table") + // Escape any double quotes within the identifier by doubling them return identifier .split('.') - .map((part) => `"${part}"`) + .map((part) => `"${part.replace(/"/g, '""')}"`) .join('.'); } From 0a628ea3bc8ff4749b0bf9132d1ffb8c9159e3a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:09:10 +0000 Subject: [PATCH 4/5] Improve error message to clarify schema-qualified names Co-authored-by: mfeltscher <1352744+mfeltscher@users.noreply.github.com> --- src/cache-tags/provider/neon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cache-tags/provider/neon.ts b/src/cache-tags/provider/neon.ts index 30f8e82..dab40ad 100644 --- a/src/cache-tags/provider/neon.ts +++ b/src/cache-tags/provider/neon.ts @@ -33,7 +33,7 @@ function quoteIdentifier(identifier: string): string { // They can also contain dots for schema-qualified names if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)?$/.test(identifier)) { throw new Error( - `Invalid table name: ${identifier}. Table names must start with a letter, underscore, or dollar sign and contain only letters, digits, underscores, and dollar signs.`, + `Invalid table name: ${identifier}. Table names must start with a letter, underscore, or dollar sign and contain only letters, digits, underscores, and dollar signs. Schema-qualified names (e.g., "schema.table") are supported.`, ); } From 08aad48e943f6779a1fcde67656390d66d02993b Mon Sep 17 00:00:00 2001 From: Moreno Feltscher Date: Mon, 16 Feb 2026 09:14:13 +0100 Subject: [PATCH 5/5] schoener --- src/cache-tags/provider/neon.ts | 64 ++++++++++++++++----------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/src/cache-tags/provider/neon.ts b/src/cache-tags/provider/neon.ts index dab40ad..64b996a 100644 --- a/src/cache-tags/provider/neon.ts +++ b/src/cache-tags/provider/neon.ts @@ -21,41 +21,16 @@ type NeonCacheTagsProviderConfig = { readonly table: string; }; -/** - * Validates and quotes a PostgreSQL identifier (table name, column name, etc.) to prevent SQL injection. - * @param identifier The identifier to validate and quote - * @returns The properly quoted identifier - * @throws Error if the identifier is invalid - */ -function quoteIdentifier(identifier: string): string { - // Validate that the identifier contains only valid characters - // PostgreSQL identifiers can contain letters, digits, underscores, and dollar signs - // They can also contain dots for schema-qualified names - if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)?$/.test(identifier)) { - throw new Error( - `Invalid table name: ${identifier}. Table names must start with a letter, underscore, or dollar sign and contain only letters, digits, underscores, and dollar signs. Schema-qualified names (e.g., "schema.table") are supported.`, - ); - } - - // Quote the identifier using double quotes to prevent SQL injection - // Handle schema-qualified names (e.g., "schema.table") - // Escape any double quotes within the identifier by doubling them - return identifier - .split('.') - .map((part) => `"${part.replace(/"/g, '""')}"`) - .join('.'); -} - /** * A `CacheTagsProvider` implementation that uses Neon as the storage backend. */ export class NeonCacheTagsProvider implements CacheTagsProvider { private readonly sql; - private readonly quotedTable; + private readonly table; constructor({ connectionUrl, table }: NeonCacheTagsProviderConfig) { this.sql = neon(connectionUrl, { fullResults: true }); - this.quotedTable = quoteIdentifier(table); + this.table = NeonCacheTagsProvider.quoteIdentifier(table); } public async storeQueryCacheTags(queryId: string, cacheTags: CacheTag[]) { @@ -66,7 +41,7 @@ export class NeonCacheTagsProvider implements CacheTagsProvider { const tags = cacheTags.flatMap((_, i) => [queryId, cacheTags[i]]); const placeholders = cacheTags.map((_, i) => `($${2 * i + 1}, $${2 * i + 2})`).join(','); - await this.sql.query(`INSERT INTO ${this.quotedTable} VALUES ${placeholders} ON CONFLICT DO NOTHING`, tags); + await this.sql.query(`INSERT INTO ${this.table} VALUES ${placeholders} ON CONFLICT DO NOTHING`, tags); } public async queriesReferencingCacheTags(cacheTags: CacheTag[]): Promise { @@ -77,7 +52,7 @@ export class NeonCacheTagsProvider implements CacheTagsProvider { const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(','); const { rows } = await this.sql.query( - `SELECT DISTINCT query_id FROM ${this.quotedTable} WHERE cache_tag IN (${placeholders})`, + `SELECT DISTINCT query_id FROM ${this.table} WHERE cache_tag IN (${placeholders})`, cacheTags, ); @@ -96,12 +71,35 @@ export class NeonCacheTagsProvider implements CacheTagsProvider { } const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(','); - return ( - (await this.sql.query(`DELETE FROM ${this.quotedTable} WHERE cache_tag IN (${placeholders})`, cacheTags)).rowCount ?? 0 - ); + return (await this.sql.query(`DELETE FROM ${this.table} WHERE cache_tag IN (${placeholders})`, cacheTags)).rowCount ?? 0; } public async truncateCacheTags() { - return (await this.sql.query(`DELETE FROM ${this.quotedTable}`)).rowCount ?? 0; + return (await this.sql.query(`DELETE FROM ${this.table}`)).rowCount ?? 0; + } + + /** + * Validates and quotes a PostgreSQL identifier (table name, column name, etc.) to prevent SQL injection. + * @param identifier The identifier to validate and quote + * @returns The properly quoted identifier + * @throws Error if the identifier is invalid + */ + private static quoteIdentifier(identifier: string): string { + // Validate that the identifier contains only valid characters + // PostgreSQL identifiers can contain letters, digits, underscores, and dollar signs + // They can also contain dots for schema-qualified names + if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)?$/.test(identifier)) { + throw new Error( + `Invalid table name: ${identifier}. Table names must start with a letter, underscore, or dollar sign and contain only letters, digits, underscores, and dollar signs. Schema-qualified names (e.g., "schema.table") are supported.`, + ); + } + + // Quote the identifier using double quotes to prevent SQL injection + // Handle schema-qualified names (e.g., "schema.table") + // Escape any double quotes within the identifier by doubling them + return identifier + .split('.') + .map((part) => `"${part.replace(/"/g, '""')}"`) + .join('.'); } }