Skip to content

Commit fc306fa

Browse files
committed
Contextual string tags to prevent SQL injection
https://nodesecroadmap.fyi/chapter-7/query-langs.html describes this approach as part of a larger discussion about library support for safe coding practices. This is one step in a larger effort to enable connection.query`SELECT * FROM T WHERE x = ${x}, y = ${y}, z = ${z}`(callback) and similar idioms. This was broken out of mysqljs/mysql#1926
1 parent 8f193ca commit fc306fa

18 files changed

+1683
-3
lines changed

README.md

+88-2
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,16 @@ console.log(sql); // UPDATE posts SET modified = CURRENT_TIMESTAMP() WHERE id =
102102
```
103103

104104
To generate objects with a `toSqlString` method, the `SqlString.raw()` method can
105-
be used. This creates an object that will be left un-touched when using in a `?`
105+
be used. This creates an object that will be left un-touched when used in a `?`
106106
placeholder, useful for using functions as dynamic values:
107107

108108
**Caution** The string provided to `SqlString.raw()` will skip all escaping
109109
functions when used, so be careful when passing in unvalidated input.
110110

111+
Similarly, `SqlString.identifier(id, forbidQualified)` creates an object with a
112+
`toSqlString` method that returns `SqlString.escapeId(id, forbidQualified)`.
113+
Its result is not re-escaped when used in a `?` or `??` placeholder.
114+
111115
```js
112116
var CURRENT_TIMESTAMP = SqlString.raw('CURRENT_TIMESTAMP()');
113117
var sql = SqlString.format('UPDATE posts SET modified = ? WHERE id = ?', [CURRENT_TIMESTAMP, 42]);
@@ -150,6 +154,15 @@ var sql = 'SELECT * FROM posts ORDER BY ' + SqlString.escapeId(sorter, true);
150154
console.log(sql); // SELECT * FROM posts ORDER BY `date.2`
151155
```
152156

157+
If `escapeId` receives an object with a `toSqlString` method, then `escapeId` uses
158+
that method's result after coercing it to a string.
159+
160+
```js
161+
var sorter = SqlString.identifier('date'); // ({ toSqlString: () => '`date`' })
162+
var sql = 'SELECT * FROM posts ORDER BY ' + sqlString.escapeId(sorter);
163+
console.log(sql); // SELECT * FROM posts ORDER BY `date`
164+
```
165+
153166
Alternatively, you can use `??` characters as placeholders for identifiers you would
154167
like to have escaped like this:
155168

@@ -161,7 +174,8 @@ console.log(sql); // SELECT `username`, `email` FROM `users` WHERE id = 1
161174
```
162175
**Please note that this last character sequence is experimental and syntax might change**
163176

164-
When you pass an Object to `.escape()` or `.format()`, `.escapeId()` is used to avoid SQL injection in object keys.
177+
When you pass an Object to `.escape()` or `.format()`, `.escapeId()`
178+
is used to avoid SQL injection in object keys.
165179

166180
### Formatting queries
167181

@@ -191,6 +205,78 @@ var sql = SqlString.format('UPDATE ?? SET ? WHERE `id` = ?', ['users', data,
191205
console.log(sql); // UPDATE `users` SET `email` = '[email protected]', `modified` = NOW() WHERE `id` = 1
192206
```
193207

208+
### ES6 Template Tag Support
209+
210+
`SqlString.sql` works as a template tag in Node versions that support ES6 features
211+
(node runtime versions 6 and later).
212+
213+
```es6
214+
var column = 'users';
215+
var userId = 1;
216+
var data = { email: '[email protected]', modified: SqlString.raw('NOW()') };
217+
var fromFormat = SqlString.format('UPDATE ?? SET ? WHERE `id` = ?', [column, data, userId]);
218+
var fromTag = SqlString.sql`UPDATE \`${column}\` SET ${data} WHERE \`id\` = ${userId}`;
219+
220+
console.log(fromFormat);
221+
console.log(fromTag.toSqlString());
222+
// Both emit:
223+
// UPDATE `users` SET `email` = '[email protected]', `modified` = NOW() WHERE `id` = 1
224+
```
225+
226+
227+
There are some differences between `SqlString.format` and `SqlString.raw`:
228+
229+
* The `SqlString.sql` tag returns a raw chunk SQL as if by `SqlString.raw`,
230+
whereas `SqlString.format` returns a string.
231+
This allows chaining:
232+
```es6
233+
let data = { a: 1 };
234+
let whereClause = SqlString.sql`WHERE ${data}`;
235+
SqlString.sql`SELECT * FROM TABLE ${whereClause}`.toSqlString();
236+
// SELECT * FROM TABLE WHERE `a` = 1
237+
```
238+
* An interpolation in a quoted string will not insert excess quotes:
239+
```es6
240+
SqlString.sql`SELECT '${ 'foo' }' `.toSqlString() === `SELECT 'foo' `;
241+
SqlString.sql`SELECT ${ 'foo' } `.toSqlString() === `SELECT 'foo' `;
242+
SqlString.format("SELECT '?' ", ['foo']) === `SELECT ''foo'' `;
243+
```
244+
This means that you can interpolate a string into an ID thus:
245+
```es6
246+
SqlString.sql`SELECT * FROM \`${ 'table' }\``.toSqlString() === 'SELECT * FROM `table`'
247+
SqlString.format('SELECT * FROM ??', ['table']) === 'SELECT * FROM `table`'
248+
```
249+
* Backticks end a template tag, so you need to escape backticks.
250+
```es6
251+
SqlString.sql`SELECT \`${ 'id' }\` FROM \`TABLE\``.toSqlString()
252+
=== 'SELECT `id` FROM `TABLE`'
253+
```
254+
* Other escape sequences are raw.
255+
```es6
256+
SqlString.sql`SELECT "\n"`.toSqlString() === 'SELECT "\\n"'
257+
SqlString.format('SELECT "\n"', []) === 'SELECT "\n"'
258+
SqlString.format(String.raw`SELECT "\n"`, []) === 'SELECT "\\n"'
259+
```
260+
* `SqlString.format` takes options at the end, but `SqlString.sql`
261+
takes an options object in a separate call.
262+
```es6
263+
let timeZone = 'GMT';
264+
let date = new Date(Date.UTC(2000, 0, 1));
265+
SqlString.sql({ timeZone })`SELECT ${date}`.toSqlString() ===
266+
'SELECT \'2000-01-01 00:00:00.000\'';
267+
SqlString.format('SELECT ?', [date], false, timezone) ===
268+
'SELECT \'2000-01-01 00:00:00.000\'';
269+
```
270+
The options object can contain any of
271+
`{ stringifyObjects, timeZone, forbidQualified }` which have the
272+
same meaning as when used with other `SqlString` APIs.
273+
274+
`SqlString.sql` handles `${...}` inside quoted strings as if the tag
275+
matched the following grammar:
276+
277+
[![Railroad Diagram](docs/sql-railroad.svg)](docs/sql-railroad.svg)
278+
279+
194280
## License
195281

196282
[MIT](LICENSE)

0 commit comments

Comments
 (0)