From 37ffb41c40e4a8c97f25813f509f3b717b39d3b6 Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:37:08 -0300 Subject: [PATCH 1/5] fix: ensure correct behavior for falsy `stringifyObjects` values --- src/index.ts | 2 +- test/falsy.test.ts | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 test/falsy.test.ts diff --git a/src/index.ts b/src/index.ts index 72234df..7854bf0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -464,7 +464,7 @@ export const format = ( ) { escapedValue = objectToValues(currentValue, timezone); setIndex = -1; - } else escapedValue = escape(currentValue, stringifyObjects, timezone); + } else escapedValue = escape(currentValue, true, timezone); } else escapedValue = escape(currentValue, stringifyObjects, timezone); result += sql.slice(chunkIndex, placeholderPosition); diff --git a/test/falsy.test.ts b/test/falsy.test.ts new file mode 100644 index 0000000..dce9da7 --- /dev/null +++ b/test/falsy.test.ts @@ -0,0 +1,77 @@ +import { assert, describe, it } from 'poku'; +import { format } from '../src/index.ts'; + +describe('Safe SET with object parameter', () => { + const sql = 'UPDATE users SET ?'; + const values = [{ name: 'foo', email: 'bar@test.com' }]; + const expected = "UPDATE users SET `name` = 'foo', `email` = 'bar@test.com'"; + + it('should expand object to key-value pairs when there is no stringifyObjects', () => { + assert.strictEqual(format(sql, values), expected); + }); + + it('should expand object to key-value pairs when stringifyObjects is undefined', () => { + assert.strictEqual(format(sql, values, undefined), expected); + }); + + it('should expand object to key-value pairs when stringifyObjects is null', () => { + // @ts-expect-error: testing null as a falsy runtime value + assert.strictEqual(format(sql, values, null), expected); + }); + + it('should expand object to key-value pairs when stringifyObjects is false', () => { + assert.strictEqual(format(sql, values, false), expected); + }); + + it('should expand object to key-value pairs when stringifyObjects is 0', () => { + // @ts-expect-error: testing 0 as a falsy runtime value + assert.strictEqual(format(sql, values, 0), expected); + }); + + it('should expand object to key-value pairs when stringifyObjects is empty string', () => { + // @ts-expect-error: testing empty string as a falsy runtime value + assert.strictEqual(format(sql, values, ''), expected); + }); + + it('should expand object to key-value pairs when stringifyObjects is omitted', () => { + assert.strictEqual(format(sql, values), expected); + }); +}); + +describe("Can't bypass via object password injection", () => { + const sql = 'SELECT * FROM `users` WHERE `username` = ? AND `password` = ?'; + const values: [string, { password: boolean }] = ['admin', { password: true }]; + const expected = + "SELECT * FROM `users` WHERE `username` = 'admin' AND `password` = '[object Object]'"; + + it('should not generate a SQL fragment when there is no stringifyObjects', () => { + assert.strictEqual(format(sql, values), expected); + }); + + it('should not generate a SQL fragment when stringifyObjects is undefined', () => { + assert.strictEqual(format(sql, values, undefined), expected); + }); + + it('should not generate a SQL fragment when stringifyObjects is null', () => { + // @ts-expect-error: testing null as a falsy runtime value + assert.strictEqual(format(sql, values, null), expected); + }); + + it('should not generate a SQL fragment when stringifyObjects is false', () => { + assert.strictEqual(format(sql, values, false), expected); + }); + + it('should not generate a SQL fragment when stringifyObjects is 0', () => { + // @ts-expect-error: testing 0 as a falsy runtime value + assert.strictEqual(format(sql, values, 0), expected); + }); + + it('should not generate a SQL fragment when stringifyObjects is empty string', () => { + // @ts-expect-error: testing empty string as a falsy runtime value + assert.strictEqual(format(sql, values, ''), expected); + }); + + it('should not generate a SQL fragment when stringifyObjects is omitted', () => { + assert.strictEqual(format(sql, values), expected); + }); +}); From 3dc8964d198bc9a5307aa4860fa0ef3e99e914d9 Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:39:02 -0300 Subject: [PATCH 2/5] test: remove reduntant tests --- test/falsy.test.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/falsy.test.ts b/test/falsy.test.ts index dce9da7..54a5e50 100644 --- a/test/falsy.test.ts +++ b/test/falsy.test.ts @@ -6,10 +6,6 @@ describe('Safe SET with object parameter', () => { const values = [{ name: 'foo', email: 'bar@test.com' }]; const expected = "UPDATE users SET `name` = 'foo', `email` = 'bar@test.com'"; - it('should expand object to key-value pairs when there is no stringifyObjects', () => { - assert.strictEqual(format(sql, values), expected); - }); - it('should expand object to key-value pairs when stringifyObjects is undefined', () => { assert.strictEqual(format(sql, values, undefined), expected); }); @@ -44,10 +40,6 @@ describe("Can't bypass via object password injection", () => { const expected = "SELECT * FROM `users` WHERE `username` = 'admin' AND `password` = '[object Object]'"; - it('should not generate a SQL fragment when there is no stringifyObjects', () => { - assert.strictEqual(format(sql, values), expected); - }); - it('should not generate a SQL fragment when stringifyObjects is undefined', () => { assert.strictEqual(format(sql, values, undefined), expected); }); From 60a35d21471e3e5d016003437afafb4dcd7a3b02 Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:45:43 -0300 Subject: [PATCH 3/5] docs: update benchmark results --- README.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d899928..2478b54 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - **TypeScript** by default. - Support for `Uint8Array` and `BigInt`. - Support for both **CJS** and **ESM** exports. -- Up to [**~40% faster**](#performance) compared to **sqlstring**. +- Up to [**~50% faster**](#performance) compared to **sqlstring**. - Distinguishes when a keyword is used as value. - Distinguishes when a column has a keyword name. - Distinguishes between multiple clauses/keywords in the same query. @@ -55,14 +55,12 @@ deno add npm:sql-escaper ### [MySQL2](https://github.com/sidorares/node-mysql2) -🚧 For **MySQL2**, it already uses **SQL Escaper** as its default escaping library since version `3.17.0`, so you just need to update it to the latest version: +For **MySQL2**, it already uses **SQL Escaper** as its default escaping library since version `3.17.0`, so you just need to update it to the latest version: ```bash -npm i mysql2@latest # soon +npm i mysql2@latest ``` -- Check the progress migration in [sidorares/node-mysql2#4054](https://github.com/sidorares/node-mysql2/pull/4054). - ### [mysqljs/mysql](https://github.com/mysqljs/mysql) You can use an overrides in your _package.json_: @@ -83,6 +81,8 @@ You can use an overrides in your _package.json_: ## Usage +For _up-to-date_ documentation, always follow the [**README.md**](https://github.com/mysqljs/sql-escaper?tab=readme-ov-file#readme) in the **GitHub** repository. + ### Quickstart ```js @@ -104,8 +104,6 @@ escape(raw('NOW()')); // => 'NOW()' ``` -> For _up-to-date_ documentation, always follow the [**README.md**](https://github.com/mysqljs/sql-escaper?tab=readme-ov-file#readme) in the **GitHub** repository. - ### Import #### ES Modules @@ -355,12 +353,12 @@ Each benchmark formats `10,000` queries using `format` with `100` mixed values ( | Benchmark | sqlstring | SQL Escaper | Difference | | ---------------------------------------- | --------: | ----------: | ---------------: | -| Select 100 values | 248.8 ms | 178.7 ms | **1.39x faster** | -| Insert 100 values | 247.5 ms | 196.2 ms | **1.26x faster** | -| SET with 100 values | 257.5 ms | 205.2 ms | **1.26x faster** | -| SET with 100 objects | 348.3 ms | 250.5 ms | **1.39x faster** | -| ON DUPLICATE KEY UPDATE with 100 values | 466.2 ms | 394.6 ms | **1.18x faster** | -| ON DUPLICATE KEY UPDATE with 100 objects | 558.2 ms | 433.9 ms | **1.29x faster** | +| Select 100 values | 264.6 ms | 170.3 ms | **1.55x faster** | +| Insert 100 values | 266.0 ms | 189.2 ms | **1.41x faster** | +| SET with 100 values | 273.9 ms | 196.1 ms | **1.40x faster** | +| SET with 100 objects | 360.7 ms | 249.3 ms | **1.45x faster** | +| ON DUPLICATE KEY UPDATE with 100 values | 515.7 ms | 375.9 ms | **1.37x faster** | +| ON DUPLICATE KEY UPDATE with 100 objects | 598.4 ms | 441.5 ms | **1.36x faster** | - See detailed results and how the benchmarks are run in the [**benchmark**](https://github.com/mysqljs/sql-escaper/tree/main/benchmark) directory. From b5527e70ff148e44b65c1a09f060b2ad75151cac Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:49:07 -0300 Subject: [PATCH 4/5] docs: prefer realistic benchmark results --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2478b54..b550849 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - **TypeScript** by default. - Support for `Uint8Array` and `BigInt`. - Support for both **CJS** and **ESM** exports. -- Up to [**~50% faster**](#performance) compared to **sqlstring**. +- Up to [**~40% faster**](#performance) compared to **sqlstring**. - Distinguishes when a keyword is used as value. - Distinguishes when a column has a keyword name. - Distinguishes between multiple clauses/keywords in the same query. @@ -353,12 +353,12 @@ Each benchmark formats `10,000` queries using `format` with `100` mixed values ( | Benchmark | sqlstring | SQL Escaper | Difference | | ---------------------------------------- | --------: | ----------: | ---------------: | -| Select 100 values | 264.6 ms | 170.3 ms | **1.55x faster** | -| Insert 100 values | 266.0 ms | 189.2 ms | **1.41x faster** | -| SET with 100 values | 273.9 ms | 196.1 ms | **1.40x faster** | -| SET with 100 objects | 360.7 ms | 249.3 ms | **1.45x faster** | -| ON DUPLICATE KEY UPDATE with 100 values | 515.7 ms | 375.9 ms | **1.37x faster** | -| ON DUPLICATE KEY UPDATE with 100 objects | 598.4 ms | 441.5 ms | **1.36x faster** | +| Select 100 values | 248.8 ms | 178.7 ms | **1.39x faster** | +| Insert 100 values | 247.5 ms | 196.2 ms | **1.26x faster** | +| SET with 100 values | 257.5 ms | 205.2 ms | **1.26x faster** | +| SET with 100 objects | 348.3 ms | 250.5 ms | **1.39x faster** | +| ON DUPLICATE KEY UPDATE with 100 values | 466.2 ms | 394.6 ms | **1.18x faster** | +| ON DUPLICATE KEY UPDATE with 100 objects | 558.2 ms | 433.9 ms | **1.29x faster** | - See detailed results and how the benchmarks are run in the [**benchmark**](https://github.com/mysqljs/sql-escaper/tree/main/benchmark) directory. From 7e6bf3dd6f2b6d2a58e0e8ab9f5cf9c3679eb8c2 Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:02:54 -0300 Subject: [PATCH 5/5] test: add `escape` test cases --- test/escape.test.ts | 71 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 test/escape.test.ts diff --git a/test/escape.test.ts b/test/escape.test.ts new file mode 100644 index 0000000..6213e67 --- /dev/null +++ b/test/escape.test.ts @@ -0,0 +1,71 @@ +import { assert, describe, it } from 'poku'; +import { escape } from '../src/index.ts'; + +describe("Can't bypass via object injection using escape directly", () => { + const value = { password: 1 }; + const expected = "'[object Object]'"; + + it('should stringify object when stringifyObjects is true', () => { + assert.strictEqual(escape(value, true), expected); + }); + + it('should stringify object when stringifyObjects is false', () => { + assert.strictEqual(escape(value, false), expected); + }); + + it('should stringify object when stringifyObjects is 0', () => { + // @ts-expect-error: testing 0 as a falsy runtime value + assert.strictEqual(escape(value, 0), expected); + }); + + it('should stringify object when stringifyObjects is empty string', () => { + // @ts-expect-error: testing empty string as a falsy runtime value + assert.strictEqual(escape(value, ''), expected); + }); +}); + +describe('Object expansion when stringifyObjects is nullish', () => { + const value = { password: 1 }; + const expanded = '`password` = 1'; + + it('should expand object when stringifyObjects is undefined', () => { + assert.strictEqual(escape(value, undefined), expanded); + }); + + it('should expand object when stringifyObjects is null', () => { + // @ts-expect-error: testing null as a falsy runtime value + assert.strictEqual(escape(value, null), expanded); + }); + + it('should expand object when stringifyObjects is omitted', () => { + assert.strictEqual(escape(value), expanded); + }); +}); + +describe('Safe object to key-value expansion for SET clauses', () => { + it('should expand single key-value pair', () => { + assert.strictEqual(escape({ name: 'foo' }), "`name` = 'foo'"); + }); + + it('should expand multiple key-value pairs', () => { + assert.strictEqual( + escape({ name: 'foo', email: 'bar@test.com' }), + "`name` = 'foo', `email` = 'bar@test.com'" + ); + }); + + it('should expand mixed value types', () => { + assert.strictEqual( + escape({ name: 'foo', active: true, age: 30 }), + "`name` = 'foo', `active` = true, `age` = 30" + ); + }); + + it('should skip function values', () => { + assert.strictEqual(escape({ name: 'foo', fn: () => {} }), "`name` = 'foo'"); + }); + + it('should return empty string for empty object', () => { + assert.strictEqual(escape({}), ''); + }); +});