diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 3a5ef27423..93f1b9075e 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -7,3 +7,7 @@ **Vulnerability:** The daemon configuration file (`~/.agor/config.yaml`) and its parent directory (`~/.agor`) were created with default file permissions (e.g., `0o755`/`0o644`), which made them readable by other users on the system. This file stores extremely sensitive information such as API keys and master JWT secrets. **Learning:** Default Node.js filesystem operations (`fs.writeFile` and `fs.mkdir`) do not enforce strict permissions unless explicitly specified with a `mode` parameter. When handling sensitive files, relying on the system `umask` is insufficient. **Prevention:** Always specify `mode: 0o600` for sensitive files and `mode: 0o700` for their parent directories. Additionally, use `fs.chmod` to retroactively secure existing files and directories that might have been created with permissive defaults. +## 2024-07-28 - [CRITICAL] SQL Injection Risk via sql.raw in jsonExtract function +**Vulnerability:** A SQL injection vulnerability existed in `packages/core/src/db/database-wrapper.ts` where `jsonExtract` used `sql.raw` to construct JSON path expressions (e.g. `->>'key'`). By interpolating string values into `sql.raw`, it bypassed Drizzle's parameterization, exposing the app to SQL injection if user input were passed as a path key. +**Learning:** Using `sql.raw()` to interpolate dynamic string variables bypasses parameterized queries. For PostgreSQL JSON operators, Drizzle requires explicit type casting of dynamic variables to resolve the overloaded operator type without relying on `sql.raw()`. +**Prevention:** In Drizzle ORM, never use `sql.raw()` to interpolate dynamic variables. To prevent SQL injection when working with PostgreSQL JSON operators (`->`, `->>`), always explicitly cast dynamic parameters to text (e.g., `sql\`->> (${key}::text)\``) to properly resolve the overloaded operator type. Use `sql.join` to safely combine these parameterized template literals when chaining multi-level paths. diff --git a/packages/core/src/db/database-wrapper.ts b/packages/core/src/db/database-wrapper.ts index d52d553e64..b4512615d0 100644 --- a/packages/core/src/db/database-wrapper.ts +++ b/packages/core/src/db/database-wrapper.ts @@ -105,15 +105,15 @@ export function jsonExtract(db: Database, column: SQL.Aliased | SQL | any, path: } else { // PostgreSQL: column->'path'->'to'->>'field' // Use -> for all but the last part (keeps as JSON), ->> for the last part (extracts as text) - // IMPORTANT: Use sql.raw() for JSON keys to avoid parameterization + // IMPORTANT: Use explicit text casting for JSON keys to allow parameterization while properly resolving the overloaded operator type if (parts.length === 1) { // Single level: column->>'key' - return sql`${column}${sql.raw(`->>'${parts[0]}'`)}`; + return sql`${column}->>(${parts[0]}::text)`; } else { // Multiple levels: column->'key1'->'key2'->>'key3' - const objectParts = parts.slice(0, -1).map((p) => sql.raw(`->'${p}'`)); + const objectParts = parts.slice(0, -1).map((p) => sql`->(${p}::text)`); const lastPart = parts[parts.length - 1]; - return sql`${column}${sql.join(objectParts, sql``)}${sql.raw(`->>'${lastPart}'`)}`; + return sql`${column}${sql.join(objectParts, sql``)}->>(${lastPart}::text)`; } } } diff --git a/packages/core/test-drizzle.ts b/packages/core/test-drizzle.ts new file mode 100644 index 0000000000..28a7a2d0c6 --- /dev/null +++ b/packages/core/test-drizzle.ts @@ -0,0 +1,9 @@ +import { sql } from 'drizzle-orm'; +import { PgDialect } from 'drizzle-orm/pg-core'; + +const dialect = new PgDialect(); +const column = sql`my_column`; +const p = "user's key"; // testing SQL injection +const result = sql`${column} -> (${p}::text)`; +const query = dialect.sqlToQuery(result); +console.log(query); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94c9d41fd3..00b33894e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1453,11 +1453,11 @@ packages: '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' + deprecated: 'Merged into tsx: https://tsx.hirok.io' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' + deprecated: 'Merged into tsx: https://tsx.hirok.io' '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} @@ -4245,69 +4245,90 @@ packages: '@tsparticles/basic@3.9.1': resolution: {integrity: sha512-ijr2dHMx0IQHqhKW3qA8tfwrR2XYbbWYdaJMQuBo2CkwBVIhZ76U+H20Y492j/NXpd1FUnt2aC0l4CEVGVGdeQ==} + deprecated: Please update to version 4.1.1 '@tsparticles/engine@3.9.1': resolution: {integrity: sha512-DpdgAhWMZ3Eh2gyxik8FXS6BKZ8vyea+Eu5BC4epsahqTGY9V3JGGJcXC6lRJx6cPMAx1A0FaQAojPF3v6rkmQ==} + deprecated: Please update to version 4.1.1 '@tsparticles/interaction-external-attract@3.9.1': resolution: {integrity: sha512-5AJGmhzM9o4AVFV24WH5vSqMBzOXEOzIdGLIr+QJf4fRh9ZK62snsusv/ozKgs2KteRYQx+L7c5V3TqcDy2upg==} + deprecated: Please update to version 4.1.1 '@tsparticles/interaction-external-bounce@3.9.1': resolution: {integrity: sha512-bv05+h70UIHOTWeTsTI1AeAmX6R3s8nnY74Ea6p6AbQjERzPYIa0XY19nq/hA7+Nrg+EissP5zgoYYeSphr85A==} + deprecated: Please update to version 4.1.1 '@tsparticles/interaction-external-bubble@3.9.1': resolution: {integrity: sha512-tbd8ox/1GPl+zr+KyHQVV1bW88GE7OM6i4zql801YIlCDrl9wgTDdDFGIy9X7/cwTvTrCePhrfvdkUamXIribQ==} + deprecated: Please update to version 4.1.1 '@tsparticles/interaction-external-connect@3.9.1': resolution: {integrity: sha512-sq8YfUNsIORjXHzzW7/AJQtfi/qDqLnYG2qOSE1WOsog39MD30RzmiOloejOkfNeUdcGUcfsDgpUuL3UhzFUOA==} + deprecated: Please update to version 4.1.1 '@tsparticles/interaction-external-grab@3.9.1': resolution: {integrity: sha512-QwXza+sMMWDaMiFxd8y2tJwUK6c+nNw554+/9+tEZeTTk2fCbB0IJ7p/TH6ZGWDL0vo2muK54Njv2fEey191ow==} + deprecated: Please update to version 4.1.1 '@tsparticles/interaction-external-pause@3.9.1': resolution: {integrity: sha512-Gzv4/FeNir0U/tVM9zQCqV1k+IAgaFjDU3T30M1AeAsNGh/rCITV2wnT7TOGFkbcla27m4Yxa+Fuab8+8pzm+g==} + deprecated: Please update to version 4.1.1 '@tsparticles/interaction-external-push@3.9.1': resolution: {integrity: sha512-GvnWF9Qy4YkZdx+WJL2iy9IcgLvzOIu3K7aLYJFsQPaxT8d9TF8WlpoMlWKnJID6H5q4JqQuMRKRyWH8aAKyQw==} + deprecated: Please update to version 4.1.1 '@tsparticles/interaction-external-remove@3.9.1': resolution: {integrity: sha512-yPThm4UDWejDOWW5Qc8KnnS2EfSo5VFcJUQDWc1+Wcj17xe7vdSoiwwOORM0PmNBzdDpSKQrte/gUnoqaUMwOA==} + deprecated: Please update to version 4.1.1 '@tsparticles/interaction-external-repulse@3.9.1': resolution: {integrity: sha512-/LBppXkrMdvLHlEKWC7IykFhzrz+9nebT2fwSSFXK4plEBxDlIwnkDxd3FbVOAbnBvx4+L8+fbrEx+RvC8diAw==} + deprecated: Please update to version 4.1.1 '@tsparticles/interaction-external-slow@3.9.1': resolution: {integrity: sha512-1ZYIR/udBwA9MdSCfgADsbDXKSFS0FMWuPWz7bm79g3sUxcYkihn+/hDhc6GXvNNR46V1ocJjrj0u6pAynS1KQ==} + deprecated: Please update to version 4.1.1 '@tsparticles/interaction-particles-attract@3.9.1': resolution: {integrity: sha512-CYYYowJuGwRLUixQcSU/48PTKM8fCUYThe0hXwQ+yRMLAn053VHzL7NNZzKqEIeEyt5oJoy9KcvubjKWbzMBLQ==} + deprecated: Please update to version 4.1.1 '@tsparticles/interaction-particles-collisions@3.9.1': resolution: {integrity: sha512-ggGyjW/3v1yxvYW1IF1EMT15M6w31y5zfNNUPkqd/IXRNPYvm0Z0ayhp+FKmz70M5p0UxxPIQHTvAv9Jqnuj8w==} + deprecated: Please update to version 4.1.1 '@tsparticles/interaction-particles-links@3.9.1': resolution: {integrity: sha512-MsLbMjy1vY5M5/hu/oa5OSRZAUz49H3+9EBMTIOThiX+a+vpl3sxc9AqNd9gMsPbM4WJlub8T6VBZdyvzez1Vg==} + deprecated: Please update to version 4.1.1 '@tsparticles/move-base@3.9.1': resolution: {integrity: sha512-X4huBS27d8srpxwOxliWPUt+NtCwY+8q/cx1DvQxyqmTA8VFCGpcHNwtqiN+9JicgzOvSuaORVqUgwlsc7h4pQ==} + deprecated: this package was deprecated in tsparticles v4, replaced by @tsparticles/plugin-move '@tsparticles/move-parallax@3.9.1': resolution: {integrity: sha512-whlOR0bVeyh6J/hvxf/QM3DqvNnITMiAQ0kro6saqSDItAVqg4pYxBfEsSOKq7EhjxNvfhhqR+pFMhp06zoCVA==} + deprecated: this package was deprecated in tsparticles v4, replaced by @tsparticles/interaction-external-parallax '@tsparticles/plugin-easing-quad@3.9.1': resolution: {integrity: sha512-C2UJOca5MTDXKUTBXj30Kiqr5UyID+xrY/LxicVWWZPczQW2bBxbIbfq9ULvzGDwBTxE2rdvIB8YFKmDYO45qw==} + deprecated: Please update to version 4.1.1 '@tsparticles/plugin-hex-color@3.9.1': resolution: {integrity: sha512-vZgZ12AjUicJvk7AX4K2eAmKEQX/D1VEjEPFhyjbgI7A65eX72M465vVKIgNA6QArLZ1DLs7Z787LOE6GOBWsg==} '@tsparticles/plugin-hsl-color@3.9.1': resolution: {integrity: sha512-jJd1iGgRwX6eeNjc1zUXiJivaqC5UE+SC2A3/NtHwwoQrkfxGWmRHOsVyLnOBRcCPgBp/FpdDe6DIDjCMO715w==} + deprecated: Please update to version 4.1.1 '@tsparticles/plugin-rgb-color@3.9.1': resolution: {integrity: sha512-SBxk7f1KBfXeTnnklbE2Hx4jBgh6I6HOtxb+Os1gTp0oaghZOkWcCD2dP4QbUu7fVNCMOcApPoMNC8RTFcy9wQ==} + deprecated: Please update to version 4.1.1 '@tsparticles/react@3.0.0': resolution: {integrity: sha512-hjGEtTT1cwv6BcjL+GcVgH++KYs52bIuQGW3PWv7z3tMa8g0bd6RI/vWSLj7p//NZ3uTjEIeilYIUPBh7Jfq/Q==} + deprecated: Please update to version 4.1.1 peerDependencies: '@tsparticles/engine': ^3.0.2 react: '>=16.8.0' @@ -4315,48 +4336,63 @@ packages: '@tsparticles/shape-circle@3.9.1': resolution: {integrity: sha512-DqZFLjbuhVn99WJ+A9ajz9YON72RtCcvubzq6qfjFmtwAK7frvQeb6iDTp6Ze9FUipluxVZWVRG4vWTxi2B+/g==} + deprecated: Please update to version 4.1.1 '@tsparticles/shape-emoji@3.9.1': resolution: {integrity: sha512-ifvY63usuT+hipgVHb8gelBHSeF6ryPnMxAAEC1RGHhhXfpSRWMtE6ybr+pSsYU52M3G9+TF84v91pSwNrb9ZQ==} + deprecated: Please update to version 4.1.1 '@tsparticles/shape-image@3.9.1': resolution: {integrity: sha512-fCA5eme8VF3oX8yNVUA0l2SLDKuiZObkijb0z3Ky0qj1HUEVlAuEMhhNDNB9E2iELTrWEix9z7BFMePp2CC7AA==} + deprecated: Please update to version 4.1.1 '@tsparticles/shape-line@3.9.1': resolution: {integrity: sha512-wT8NSp0N9HURyV05f371cHKcNTNqr0/cwUu6WhBzbshkYGy1KZUP9CpRIh5FCrBpTev34mEQfOXDycgfG0KiLQ==} + deprecated: Please update to version 4.1.1 '@tsparticles/shape-polygon@3.9.1': resolution: {integrity: sha512-dA77PgZdoLwxnliH6XQM/zF0r4jhT01pw5y7XTeTqws++hg4rTLV9255k6R6eUqKq0FPSW1/WBsBIl7q/MmrqQ==} + deprecated: Please update to version 4.1.1 '@tsparticles/shape-square@3.9.1': resolution: {integrity: sha512-DKGkDnRyZrAm7T2ipqNezJahSWs6xd9O5LQLe5vjrYm1qGwrFxJiQaAdlb00UNrexz1/SA7bEoIg4XKaFa7qhQ==} + deprecated: Please update to version 4.1.1 '@tsparticles/shape-star@3.9.1': resolution: {integrity: sha512-kdMJpi8cdeb6vGrZVSxTG0JIjCwIenggqk0EYeKAwtOGZFBgL7eHhF2F6uu1oq8cJAbXPujEoabnLsz6mW8XaA==} + deprecated: Please update to version 4.1.1 '@tsparticles/slim@3.9.1': resolution: {integrity: sha512-CL5cDmADU7sDjRli0So+hY61VMbdroqbArmR9Av+c1Fisa5ytr6QD7Jv62iwU2S6rvgicEe9OyRmSy5GIefwZw==} + deprecated: Please update to version 4.1.1 '@tsparticles/updater-color@3.9.1': resolution: {integrity: sha512-XGWdscrgEMA8L5E7exsE0f8/2zHKIqnTrZymcyuFBw2DCB6BIV+5z6qaNStpxrhq3DbIxxhqqcybqeOo7+Alpg==} + deprecated: this package was deprecated in tsparticles v4, replaced by @tsparticles/updater-paint '@tsparticles/updater-life@3.9.1': resolution: {integrity: sha512-Oi8aF2RIwMMsjssUkCB6t3PRpENHjdZf6cX92WNfAuqXtQphr3OMAkYFJFWkvyPFK22AVy3p/cFt6KE5zXxwAA==} + deprecated: Please update to version 4.1.1 '@tsparticles/updater-opacity@3.9.1': resolution: {integrity: sha512-w778LQuRZJ+IoWzeRdrGykPYSSaTeWfBvLZ2XwYEkh/Ss961InOxZKIpcS6i5Kp/Zfw0fS1ZAuqeHwuj///Osw==} + deprecated: Please update to version 4.1.1 '@tsparticles/updater-out-modes@3.9.1': resolution: {integrity: sha512-cKQEkAwbru+hhKF+GTsfbOvuBbx2DSB25CxOdhtW2wRvDBoCnngNdLw91rs+0Cex4tgEeibkebrIKFDDE6kELg==} + deprecated: Please update to version 4.1.1 '@tsparticles/updater-rotate@3.9.1': resolution: {integrity: sha512-9BfKaGfp28JN82MF2qs6Ae/lJr9EColMfMTHqSKljblwbpVDHte4umuwKl3VjbRt87WD9MGtla66NTUYl+WxuQ==} + deprecated: Please update to version 4.1.1 '@tsparticles/updater-size@3.9.1': resolution: {integrity: sha512-3NSVs0O2ApNKZXfd+y/zNhTXSFeG1Pw4peI8e6z/q5+XLbmue9oiEwoPy/tQLaark3oNj3JU7Q903ZijPyXSzw==} + deprecated: Please update to version 4.1.1 '@tsparticles/updater-stroke-color@3.9.1': resolution: {integrity: sha512-3x14+C2is9pZYTg9T2TiA/aM1YMq4wLdYaZDcHm3qO30DZu5oeQq0rm/6w+QOGKYY1Z3Htg9rlSUZkhTHn7eDA==} + deprecated: this package was deprecated in tsparticles v4, replaced by @tsparticles/updater-paint '@turbo/darwin-64@2.9.6': resolution: {integrity: sha512-X/56SnVXIQZBLKwniGTwEQTGmtE5brSACnKMBWpY3YafuxVYefrC2acamfjgxP7BG5w3I+6jf0UrLoSzgPcSJg==} @@ -4719,6 +4755,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@vitejs/plugin-react@5.1.4': resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==} diff --git a/test-drizzle-2.ts b/test-drizzle-2.ts new file mode 100644 index 0000000000..e2525998da --- /dev/null +++ b/test-drizzle-2.ts @@ -0,0 +1,11 @@ +import { sql } from 'drizzle-orm'; +import { PgDialect } from 'drizzle-orm/pg-core'; + +const dialect = new PgDialect(); +const column = sql`my_column`; +const parts = ["user's key", 'another key']; +const objectParts = parts.slice(0, -1).map((p) => sql`->(${p}::text)`); +const lastPart = parts[parts.length - 1]; +const result = sql`${column}${sql.join(objectParts, sql``)}->>(${lastPart}::text)`; +const query = dialect.sqlToQuery(result); +console.log(query); diff --git a/test-drizzle-3.ts b/test-drizzle-3.ts new file mode 100644 index 0000000000..50cc4ee1f6 --- /dev/null +++ b/test-drizzle-3.ts @@ -0,0 +1,20 @@ +import { sql } from 'drizzle-orm'; +import { PgDialect } from 'drizzle-orm/pg-core'; +import { SQLiteSyncDialect } from 'drizzle-orm/sqlite-core'; + +const pgDialect = new PgDialect(); +const sqliteDialect = new SQLiteSyncDialect(); + +const column = sql`my_column`; +const path = "path.to.my'key"; +const parts = path.split('.'); + +console.log('Postgres 1 part:'); +const p1Result = sql`${column}->>(${parts[0]}::text)`; +console.log(pgDialect.sqlToQuery(p1Result)); + +console.log('Postgres multi parts:'); +const objectParts = parts.slice(0, -1).map((p) => sql`->(${p}::text)`); +const lastPart = parts[parts.length - 1]; +const p2Result = sql`${column}${sql.join(objectParts, sql``)}->>(${lastPart}::text)`; +console.log(pgDialect.sqlToQuery(p2Result)); diff --git a/test-drizzle.ts b/test-drizzle.ts new file mode 100644 index 0000000000..41f550e75b --- /dev/null +++ b/test-drizzle.ts @@ -0,0 +1,9 @@ +import { sql } from 'drizzle-orm'; +import { PgDialect } from 'drizzle-orm/pg-core'; + +const dialect = new PgDialect(); +const column = sql`my_column`; +const p = 'key1'; +const result = sql`${column} -> (${p}::text)`; +const query = dialect.sqlToQuery(result); +console.log(query);