diff --git a/README.md b/README.md index d899928..b550849 100644 --- a/README.md +++ b/README.md @@ -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 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/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({}), ''); + }); +}); diff --git a/test/falsy.test.ts b/test/falsy.test.ts new file mode 100644 index 0000000..54a5e50 --- /dev/null +++ b/test/falsy.test.ts @@ -0,0 +1,69 @@ +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 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 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); + }); +});