├── .coveralls.yml
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── docs
├── mysql-railroad.svg
└── pg-railroad.svg
├── index.js
├── lib
├── escapers.js
├── fragment.js
├── id.js
├── mysql-escaper.js
├── mysql-lexer.js
├── pg-escaper.js
├── pg-lexer.js
└── tag-fn.js
├── package-lock.json
├── package.json
├── scripts
├── make-md-toc.pl
├── prepublish.sh
└── validate.sh
└── test
├── escapers-test.js
├── example-test.js
├── mysql-lexer-test.js
├── mysql-tag-test.js
├── pg-lexer-test.js
└── pg-tag-test.js
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: travis-pro
2 | repo_token: 2aCRkrfmsLJFPpAzjwIu5RDRBY6JQSszA
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Emacs droppings
9 | *~
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # nyc test coverage
24 | .nyc_output
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # Bower dependency directory (https://bower.io/)
30 | bower_components
31 |
32 | # node-waf configuration
33 | .lock-wscript
34 |
35 | # Compiled binary addons (https://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # TypeScript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 |
63 | # next.js build output
64 | .next
65 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "7"
4 | - "8"
5 | - "9"
6 | - "10"
7 | - "stable"
8 |
9 | # Use faster Docker architecture on Travis.
10 | sudo: false
11 |
12 | script: ./scripts/validate.sh
13 | after_success: npm run coveralls
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Safe SQL Template Tag
4 |
5 | [](https://travis-ci.org/mikesamuel/safesql)
6 | [](https://david-dm.org/mikesamuel/safesql)
7 | [](https://www.npmjs.com/package/safesql)
8 | [](https://coveralls.io/github/mikesamuel/safesql?branch=master)
9 | [](https://packagephobia.now.sh/result?p=safesql)
10 | [](https://snyk.io/test/github/mikesamuel/safesql?targetFile=package.json)
11 |
12 | Provides a string template tag that makes it easy to compose
13 | [MySQL][mysql] and [PostgreSQL][pg] query strings from untrusted
14 | inputs by escaping dynamic values based on the context in which they
15 | appear.
16 |
17 |
18 |
19 |
20 |
21 | * [Installation](#installation)
22 | * [Supported Databases](#supported)
23 | * [Usage By Example](#usage)
24 | * [`mysql` returns a *SqlFragment*](#sql-returns-sqlfragment)
25 | * [No excess quotes](#minimal-quotes)
26 | * [Escaped backticks delimit SQL identifiers](#escaped-backticks)
27 | * [Escape Sequences are Raw](#raw-escapes)
28 | * [API](#API)
29 | * [mysql(options)](#mysql-options)
30 | * [pgsql(options)](#pg-options)
31 | * [mysql\`...\`](#mysql-as-tag)
32 | * [pg\`...\`](#pg-as-tag)
33 | * [SqlFragment](#class-SqlFragment)
34 | * [SqlId](#class-SqlId)
35 |
36 |
37 |
38 | ## Installation
39 |
40 | ```bash
41 | $ npm install safesql
42 | ```
43 |
44 | ## Supported Databases
45 |
46 | **MySQL** via
47 |
48 | ```js
49 | const { mysql } = require('safesql');
50 | ```
51 |
52 | **PostgreSQL** via
53 |
54 | ```js
55 | const { pg } = require('safesql');
56 | ```
57 |
58 |
59 | ## Usage By Example
60 |
61 |
67 |
68 | ```js
69 | const { mysql, SqlId } = require('safesql');
70 |
71 | const table = 'table';
72 | const ids = [ 'x', 'y', 'z' ];
73 | const str = 'foo\'"bar';
74 |
75 | const query = mysql`SELECT * FROM \`${ table }\` WHERE id IN (${ ids }) AND s=${ str }`;
76 |
77 | console.log(query);
78 | // SELECT * FROM `table` WHERE id IN ('x', 'y', 'z') AND s='foo''"bar'
79 | ```
80 |
81 | `mysql` functions as a template tag.
82 |
83 | Commas separate elements of arrays in the output.
84 |
85 | `mysql` treats a `${...}` between backticks (\\\`) as a SQL identifier.
86 |
87 | A `${...}` outside any quotes will be escaped and wrapped in appropriate quotes if necessary.
88 |
89 | ----
90 |
91 | PostgreSQL differs from MySQL in important ways. Use `pg` for Postgres.
92 |
93 | ```js
94 | const { pg, SqlId } = require('safesql');
95 |
96 | const table = 'table';
97 | const ids = [ 'x', 'y', 'z' ];
98 | const str = 'foo\'"bar';
99 |
100 | const query = pg`SELECT * FROM "${ table }" WHERE id IN (${ ids }) AND s=${ str }`;
101 |
102 | console.log(query);
103 | // SELECT * FROM "table" WHERE id IN ('x', 'y', 'z') AND s=e'foo''\"bar'
104 | ```
105 |
106 | ----
107 |
108 | You can pass in an object to relate columns to values as in a `SET` clause above.
109 |
110 | The output of mysql\`...\` has type *SqlFragment* so the
111 | `NOW()` function call is not re-escaped when used in `${data}`.
112 |
113 | ```js
114 | const { mysql } = require('safesql');
115 |
116 | const column = 'users';
117 | const userId = 1;
118 | const data = {
119 | email: 'foobar@example.com',
120 | modified: mysql`NOW()`
121 | };
122 | const query = mysql`UPDATE \`${column}\` SET ${data} WHERE \`id\` = ${userId}`;
123 |
124 | console.log(query);
125 | // UPDATE `users` SET `email` = 'foobar@example.com', `modified` = NOW() WHERE `id` = 1
126 | ```
127 |
128 | ### `mysql` returns a *SqlFragment*
129 |
130 | Since `mysql` returns a *SqlFragment* you can chain uses:
131 |
132 | ```js
133 | const { mysql } = require('safesql');
134 |
135 | const data = { a: 1 };
136 | const whereClause = mysql`WHERE ${data}`;
137 | console.log(mysql`SELECT * FROM TABLE ${whereClause}`);
138 | // SELECT * FROM TABLE WHERE `a` = 1
139 | ```
140 |
141 | ### No excess quotes
142 |
143 | An interpolation in a quoted string will not insert excess quotes:
144 |
145 | ```js
146 | const { mysql } = require('safesql')
147 |
148 | console.log(mysql`SELECT '${ 'foo' }' `)
149 | // SELECT 'foo'
150 | console.log(mysql`SELECT ${ 'foo' } `)
151 | // SELECT 'foo'
152 | ```
153 |
154 | ### Escaped backticks delimit SQL identifiers
155 |
156 | Backticks end a template tag, so you need to escape backticks.
157 |
158 | ```js
159 | const { mysql } = require('safesql')
160 |
161 | console.log(mysql`SELECT \`${ 'id' }\` FROM \`TABLE\``)
162 | // SELECT `id` FROM `TABLE`
163 | ```
164 |
165 | ### Escape Sequences are Raw
166 |
167 | Other escape sequences are raw.
168 |
169 | ```js
170 | const { mysql } = require('safesql')
171 |
172 | console.log(mysql`SELECT "\n"`)
173 | // SELECT "\n"
174 | ```
175 |
176 | ## API
177 |
178 | Assuming
179 |
180 | ```js
181 | const { mysql, pg, SqlFragment, SqlId } = require('safesql')
182 | ```
183 |
184 | ### mysql(options)
185 | ### pgsql(options)
186 |
187 | When called with an options bundle instead of as a template tag,
188 | `mysql` and `pg` return a template tag that uses those options.
189 |
190 | The options object can contain any of
191 | `{ stringifyObjects, timeZone, forbidQualified }` which have the
192 | same meaning as when used with *[sqlstring][]*.
193 |
194 | ```js
195 | const timeZone = 'GMT'
196 | const date = new Date(Date.UTC(2000, 0, 1))
197 |
198 | console.log(mysql({ timeZone })`SELECT ${date}`)
199 | // SELECT '2000-01-01 00:00:00.000'
200 | ```
201 |
202 | ### mysql\`...\`
203 |
204 | When used as a template tag, chooses an appropriate escaping
205 | convention for each `${...}` based on the context in which it appears.
206 |
207 | `mysql` handles `${...}` inside quoted strings as if the template
208 | matched the following grammar:
209 |
210 | [![Railroad Diagram][mysql-railroad-raw]][mysql-railroad]
211 |
212 | ### pg\`...\`
213 |
214 | When used as a template tag, chooses an appropriate escaping
215 | convention for each `${...}` based on the context in which it appears.
216 |
217 | `pg` handles `${...}` inside quoted strings as if the template
218 | matched the following grammar:
219 |
220 | [![Railroad Diagram][pg-railroad-raw]][pg-railroad]
221 |
222 | ### SqlFragment
223 |
224 | *SqlFragment* is a [Mintable][] class that represents fragments of SQL
225 | that are safe to send to a database.
226 |
227 | See [minting][] for example on how to create instances, and why this is a
228 | tad more involved than just using `new`.
229 |
230 | ### SqlId
231 |
232 | *SqlId* is a [Mintable][] class that represents a SQL identifier.
233 |
234 | See [minting][] for example on how to create instances, and why this is a
235 | tad more involved than just using `new`.
236 |
237 | A `SqlId`'s content must be the raw text of a SQL identifier and
238 | creators should not rely on case folding by the database client.
239 |
240 |
241 | [mysql]: https://www.npmjs.com/package/mysql
242 | [pg]: https://www.npmjs.com/package/pg
243 | [sqlstring]: https://www.npmjs.com/package/sqlstring
244 | [Mintable]: https://www.npmjs.com/package/node-sec-patterns
245 | [minting]: https://www.npmjs.com/package/node-sec-patterns#creating-mintable-values
246 |
247 | [mysql-railroad]: docs/mysql-railroad.svg
248 | [mysql-railroad-raw]: https://rawgit.com/mikesamuel/safesql/master/docs/mysql-railroad.svg
249 | [pg-railroad]: docs/pg-railroad.svg
250 | [pg-railroad-raw]: https://rawgit.com/mikesamuel/safesql/master/docs/pg-railroad.svg
251 |
--------------------------------------------------------------------------------
/docs/mysql-railroad.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
90 |
91 |
92 |
856 |
--------------------------------------------------------------------------------
/docs/pg-railroad.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
110 |
111 |
112 |
1002 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | 'use strict';
19 |
20 | require('module-keys/cjs').polyfill(module, require);
21 |
22 | const { SqlFragment } = require('./lib/fragment.js');
23 | const { SqlId } = require('./lib/id.js');
24 | const { makeSqlTagFunction } = require('./lib/tag-fn.js');
25 | const { Mintable } = require('node-sec-patterns');
26 |
27 | const mintSqlFragment = require.moduleKeys.unbox(
28 | Mintable.minterFor(SqlFragment),
29 | () => true,
30 | String);
31 |
32 | let mysql = null;
33 | let pg = null; // eslint-disable-line id-length
34 |
35 | Object.defineProperties(module.exports, {
36 | mysql: {
37 | // Lazily load MySQL machinery since
38 | // PG users are unlikely to use MySQL and vice-versa.
39 | get() {
40 | if (!mysql) {
41 | // eslint-disable-next-line global-require
42 | const lexer = require('./lib/mysql-lexer.js');
43 | // eslint-disable-next-line global-require
44 | const { escape, escapeDelimited } = require('./lib/mysql-escaper.js');
45 | mysql = makeSqlTagFunction(
46 | lexer, escape, escapeDelimited, true, mintSqlFragment);
47 | }
48 | return mysql;
49 | },
50 | enumerable: true,
51 | },
52 | pg: {
53 | get() {
54 | if (!pg) {
55 | // eslint-disable-next-line global-require
56 | const lexer = require('./lib/pg-lexer.js');
57 | // eslint-disable-next-line global-require
58 | const { escape, escapeDelimited } = require('./lib/pg-escaper.js');
59 | pg = makeSqlTagFunction(
60 | lexer, escape, escapeDelimited, false, mintSqlFragment);
61 | }
62 | return pg;
63 | },
64 | enumerable: true,
65 | },
66 | SqlId: {
67 | value: SqlId,
68 | enumerable: true,
69 | },
70 | SqlFragment: {
71 | value: SqlFragment,
72 | enumerable: true,
73 | },
74 | });
75 |
--------------------------------------------------------------------------------
/lib/escapers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | 'use strict';
19 |
20 | /* eslint id-length: 0, complexity: ["error", { "max": 15 }] */
21 |
22 | const { Mintable } = require('node-sec-patterns');
23 | const { SqlFragment } = require('./fragment.js');
24 | const { SqlId } = require('./id.js');
25 |
26 | const isSqlId = Mintable.verifierFor(SqlId);
27 | const isSqlFragment = Mintable.verifierFor(SqlFragment);
28 |
29 | const iteratorSymbol = Symbol.iterator;
30 | const { isArray } = Array;
31 | const { apply } = Reflect;
32 | const { toString: bufferProtoToString } = Buffer.prototype;
33 | const { isBuffer } = Buffer;
34 |
35 | const CHARS_GLOBAL_REGEXP = /[\0\b\t\n\r\x1a"'\\$]/g; // eslint-disable-line no-control-regex
36 | const TZ_REGEXP = /([+\-\s])(\d\d):?(\d\d)?/;
37 |
38 | function isSeries(val) {
39 | // The typeof val === 'object' check prevents treating strings as series.
40 | // Per (6.1.5.1 Well-Known Symbols),
41 | // "Unless otherwise specified, well-known symbols values are shared by all realms"
42 | // so the iteratorSymbol check below should work cross-realm.
43 | // TODO: It's possible that a function might implement iterator.
44 | return val && typeof val !== 'string' && (isArray(val) || typeof val[iteratorSymbol] === 'function');
45 | }
46 |
47 | function pad(val, template) {
48 | const str = `${ val >>> 0 }`; // eslint-disable-line no-bitwise
49 | return `${ template.substring(str.length) }${ str }`;
50 | }
51 |
52 | function convertTimezone(tz) {
53 | if (tz === 'Z') {
54 | return 0;
55 | }
56 |
57 | const m = TZ_REGEXP.exec(tz);
58 | if (m) {
59 | // eslint-disable-next-line no-magic-numbers
60 | return (m[1] === '-' ? -1 : 1) * (parseInt(m[2], 10) + ((m[3] ? parseInt(m[3], 10) : 0) / 60)) * 60;
61 | }
62 | return false;
63 | }
64 |
65 | function escapeSeries(series, escapeOne, nests) {
66 | let sql = '';
67 |
68 | if (isArray(series)) {
69 | for (let i = 0, len = series.length; i < len; ++i) {
70 | const val = series[i];
71 | if (nests && isSeries(val)) {
72 | sql += `${ (i ? ', (' : '(') }${ escapeSeries(val, escapeOne, true) })`;
73 | } else {
74 | sql += `${ (i ? ', ' : '') }${ escapeOne(val) }`;
75 | }
76 | }
77 | } else {
78 | let wrote = false;
79 | for (const val of series) {
80 | if (nests && isSeries(val)) {
81 | sql += `${ (wrote ? ', (' : '(') }${ escapeSeries(val, escapeOne, true) })`;
82 | } else {
83 | sql += `${ (wrote ? ', ' : '') }${ escapeOne(val) }`;
84 | }
85 | wrote = true;
86 | }
87 | }
88 |
89 | return sql;
90 | }
91 |
92 | function bufferToString(buffer) {
93 | return `X'${ apply(bufferProtoToString, buffer, [ 'hex' ]) }'`;
94 | }
95 |
96 |
97 | function makeEscaper(escapeId, escapeString) {
98 | // eslint-disable-next-line max-params
99 | function formatDate(year, month, day, hour, minute, second, millis) {
100 | // YYYY-MM-DD HH:mm:ss.mmm
101 | return escapeString(`${ pad(year, '0000') }-${ pad(month, '00') }-${ pad(day, '00') } ${ pad(hour, '00')
102 | }:${ pad(minute, '00') }:${ pad(second, '00') }.${ pad(millis, '000') }`);
103 | }
104 |
105 | function dateToString(date, timeZone) {
106 | const dt = new Date(date);
107 |
108 | if (isNaN(dt.getTime())) {
109 | return 'NULL';
110 | }
111 |
112 | if (timeZone === 'local') {
113 | return formatDate(
114 | dt.getFullYear(),
115 | dt.getMonth() + 1,
116 | dt.getDate(),
117 | dt.getHours(),
118 | dt.getMinutes(),
119 | dt.getSeconds(),
120 | dt.getMilliseconds());
121 | }
122 |
123 | const tz = convertTimezone(timeZone);
124 |
125 | if (tz !== false && tz !== 0) {
126 | // eslint-disable-next-line no-magic-numbers
127 | dt.setTime(dt.getTime() + (tz * 60000));
128 | }
129 |
130 | return formatDate(
131 | dt.getUTCFullYear(),
132 | dt.getUTCMonth() + 1,
133 | dt.getUTCDate(),
134 | dt.getUTCHours(),
135 | dt.getUTCMinutes(),
136 | dt.getUTCSeconds(),
137 | dt.getUTCMilliseconds());
138 | }
139 |
140 | function escape(val, stringifyObjects, timeZone) {
141 | if (val === void 0 || val === null) {
142 | return 'NULL';
143 | }
144 |
145 | switch (typeof val) {
146 | case 'boolean':
147 | return (val) ? 'true' : 'false';
148 | case 'number':
149 | return `${ val }`;
150 | case 'object':
151 | break;
152 | default:
153 | return escapeString(val);
154 | }
155 | if (isSqlFragment(val)) {
156 | return val.content;
157 | }
158 | if (isSqlId(val)) {
159 | return escapeId(val.content);
160 | }
161 | if (val instanceof Date) {
162 | return dateToString(val, timeZone || 'local');
163 | }
164 | if (isBuffer(val)) {
165 | return bufferToString(val);
166 | }
167 | if (isSeries(val)) {
168 | return escapeSeries(val, (element) => escape(element, true, timeZone), true);
169 | }
170 | if (stringifyObjects) {
171 | return escapeString(val.toString());
172 | }
173 | // eslint-disable-next-line no-use-before-define
174 | return objectToValues(val, timeZone);
175 | }
176 |
177 | function objectToValues(obj, timeZone) {
178 | let sql = '';
179 |
180 | for (const key in obj) {
181 | const val = obj[key];
182 |
183 | if (typeof val === 'function') {
184 | continue;
185 | }
186 |
187 | sql += `${ (sql.length === 0 ? '' : ', ') + escapeId(key) } = ${ escape(val, true, timeZone) }`;
188 | }
189 |
190 | return sql;
191 | }
192 |
193 | return escape;
194 | }
195 |
196 | module.exports = Object.freeze({
197 | CHARS_GLOBAL_REGEXP,
198 | escapeSeries,
199 | isSeries,
200 | isSqlFragment,
201 | makeEscaper,
202 | });
203 |
--------------------------------------------------------------------------------
/lib/fragment.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | 'use strict';
19 |
20 | const { TypedString } = require('template-tag-common');
21 |
22 | class SqlFragment extends TypedString {}
23 |
24 | Object.defineProperty(
25 | SqlFragment,
26 | 'contractKey',
27 | {
28 | value: 'safesql/fragment',
29 | enumerable: true,
30 | });
31 |
32 | module.exports.SqlFragment = SqlFragment;
33 |
--------------------------------------------------------------------------------
/lib/id.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | /* eslint no-inline-comments: 0 */
19 |
20 | 'use strict';
21 |
22 | require('module-keys/cjs').polyfill(module, require);
23 |
24 | const { TypedString } = require('template-tag-common');
25 |
26 | class SqlId extends TypedString {}
27 |
28 | Object.defineProperty(
29 | SqlId,
30 | 'contractKey',
31 | {
32 | value: 'safesql/id',
33 | enumerable: true,
34 | });
35 |
36 | module.exports.SqlId = SqlId;
37 |
--------------------------------------------------------------------------------
/lib/mysql-escaper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | 'use strict';
19 |
20 | const {
21 | CHARS_GLOBAL_REGEXP,
22 | escapeSeries,
23 | isSeries,
24 | isSqlFragment,
25 | makeEscaper,
26 | } = require('./escapers.js');
27 |
28 | const { toString: bufferProtoToString } = Buffer.prototype;
29 | const { isBuffer } = Buffer;
30 | const { apply } = Reflect;
31 |
32 | const BT_GLOBAL_REGEXP = /`/g;
33 | const QUAL_GLOBAL_REGEXP = /\./g;
34 | const MYSQL_ID_REGEXP = /^`(?:[^`]|``)+`$/;
35 | const MYSQL_QUAL_ID_REGEXP = /^`(?:[^`]|``)+`(?:[.]`(?:[^`]|``)+`)*$/;
36 |
37 | const MYSQL_CHARS_ESCAPE_MAP = {
38 | __proto__: null,
39 | '\0': '\\0',
40 | '\b': '\\b',
41 | '\t': '\\t',
42 | '\n': '\\n',
43 | '\r': '\\r',
44 | // Windows end-of-file
45 | '\x1a': '\\Z',
46 | '"': '\\"',
47 | '$': '\\$',
48 | '\'': '\\\'',
49 | '\\': '\\\\',
50 | };
51 |
52 |
53 | function mysqlEscapeId(val, forbidQualified) {
54 | if (isSqlFragment(val)) {
55 | const { content } = val;
56 | if ((forbidQualified ? MYSQL_ID_REGEXP : MYSQL_QUAL_ID_REGEXP).test(content)) {
57 | return content;
58 | }
59 | throw new Error(`Expected id, got ${ content }`);
60 | }
61 | if (isSeries(val)) {
62 | return escapeSeries(val, (element) => mysqlEscapeId(element, forbidQualified), false);
63 | }
64 | if (forbidQualified) {
65 | return `\`${ String(val).replace(BT_GLOBAL_REGEXP, '``') }\``;
66 | }
67 | return `\`${ String(val).replace(BT_GLOBAL_REGEXP, '``').replace(QUAL_GLOBAL_REGEXP, '`.`') }\``;
68 | }
69 |
70 | function mysqlEscapeString(val) {
71 | const str = `${ val }`;
72 |
73 | let chunkIndex = 0;
74 | let escapedVal = '';
75 |
76 | CHARS_GLOBAL_REGEXP.lastIndex = 0;
77 | for (let match; (match = CHARS_GLOBAL_REGEXP.exec(str));) {
78 | escapedVal += str.substring(chunkIndex, match.index) + MYSQL_CHARS_ESCAPE_MAP[match[0]];
79 | chunkIndex = CHARS_GLOBAL_REGEXP.lastIndex;
80 | }
81 |
82 | if (chunkIndex === 0) {
83 | // Nothing was escaped
84 | return `'${ str }'`;
85 | }
86 |
87 | if (chunkIndex < str.length) {
88 | return `'${ escapedVal }${ str.substring(chunkIndex) }'`;
89 | }
90 |
91 | return `'${ escapedVal }'`;
92 | }
93 |
94 | const mysqlEscape = makeEscaper(mysqlEscapeId, mysqlEscapeString);
95 |
96 | function mysqlEscapeDelimited(value, delimiter, timeZone, forbidQualified) {
97 | if (delimiter === '`') {
98 | return mysqlEscapeId(value, forbidQualified).replace(/^`|`$/g, '');
99 | }
100 | if (isBuffer(value)) {
101 | value = apply(bufferProtoToString, value, [ 'binary' ]);
102 | }
103 | const escaped = mysqlEscape(String(value), true, timeZone);
104 | return escaped.substring(1, escaped.length - 1);
105 | }
106 |
107 | module.exports = Object.freeze({
108 | escape: mysqlEscape,
109 | escapeId: mysqlEscapeId,
110 | escapeDelimited: mysqlEscapeDelimited,
111 | });
112 |
--------------------------------------------------------------------------------
/lib/mysql-lexer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | 'use strict';
19 |
20 | // A simple lexer for MySQL SQL.
21 | // SQL has many divergent dialects with subtly different
22 | // conventions for string escaping and comments.
23 | // This just attempts to roughly tokenize MySQL's specific variant.
24 | // See also
25 | // https://www.w3.org/2005/05/22-SPARQL-MySQL/sql_yacc
26 | // https://github.com/twitter/mysql/blob/master/sql/sql_lex.cc
27 | // https://dev.mysql.com/doc/refman/5.7/en/string-literals.html
28 |
29 | // "--" followed by whitespace starts a line comment
30 | // "#"
31 | // "/*" starts an inline comment ended at first "*/"
32 | // \N means null
33 | // Prefixed strings x'...' is a hex string, b'...' is a binary string, ....
34 | // '...', "..." are strings. `...` escapes identifiers.
35 | // doubled delimiters and backslash both escape
36 | // doubled delimiters work in `...` identifiers
37 |
38 | // eslint-disable-next-line no-use-before-define
39 | exports.makeLexer = makeLexer;
40 |
41 | const WSP = '[\\t\\r\\n ]';
42 | const PREFIX_BEFORE_DELIMITER = new RegExp(
43 | '^(?:' +
44 |
45 | // Comment
46 | // https://dev.mysql.com/doc/refman/5.7/en/comments.html
47 | // https://dev.mysql.com/doc/refman/5.7/en/ansi-diff-comments.html
48 | // If we do not see a newline at the end of a comment, then it is
49 | // a concatenation hazard; a fragment concatened at the end would
50 | // start in a comment context.
51 | `--(?=${ WSP })[^\\r\\n]*[\r\n]` +
52 | '|#[^\\r\\n]*[\r\n]' +
53 | '|/[*][\\s\\S]*?[*]/' +
54 | '|' +
55 |
56 | // Run of non-comment non-string starts
57 | `(?:[^'"\`\\-/#]|-(?!-${ WSP })|/(?![*]))` +
58 | ')*');
59 | const DELIMITED_BODIES = {
60 | '\'': /^(?:[^'\\]|\\[\s\S]|'')*/,
61 | '"': /^(?:[^"\\]|\\[\s\S]|"")*/,
62 | '`': /^(?:[^`\\]|\\[\s\S]|``)*/,
63 | };
64 |
65 | /**
66 | * Template tag that creates a new Error with a message.
67 | * @param {!Array.} strs a valid TemplateObject.
68 | * @return {string} A message suitable for the Error constructor.
69 | */
70 | function msg(strs, ...dyn) {
71 | let message = String(strs[0]);
72 | for (let i = 0; i < dyn.length; ++i) {
73 | message += JSON.stringify(dyn[i]) + strs[i + 1];
74 | }
75 | return message;
76 | }
77 |
78 | /**
79 | * Returns a stateful function that can be fed chunks of input and
80 | * which returns a delimiter context.
81 | *
82 | * @return {!function (string) : string}
83 | * a stateful function that takes a string of SQL text and
84 | * returns the context after it. Subsequent calls will assume
85 | * that context.
86 | */
87 | function makeLexer() {
88 | let errorMessage = null;
89 | let delimiter = null;
90 | return (text) => {
91 | if (errorMessage) {
92 | // Replay the error message if we've already failed.
93 | throw new Error(errorMessage);
94 | }
95 | if (text === null) {
96 | if (delimiter) {
97 | throw new Error(
98 | errorMessage = `Unclosed quoted string: ${ delimiter }`);
99 | }
100 | }
101 | text = String(text);
102 | while (text) {
103 | const pattern = delimiter ?
104 | DELIMITED_BODIES[delimiter] :
105 | PREFIX_BEFORE_DELIMITER;
106 | const match = pattern.exec(text);
107 | // Match must be defined since all possible values of pattern have
108 | // an outer Kleene-* and no postcondition so will fallback to matching
109 | // the empty string.
110 | let nConsumed = match[0].length;
111 | if (text.length > nConsumed) {
112 | const chr = text.charAt(nConsumed);
113 | if (delimiter) {
114 | if (chr === delimiter) {
115 | delimiter = null;
116 | ++nConsumed;
117 | } else {
118 | throw new Error(
119 | errorMessage = msg`Expected ${ chr } at ${ text }`);
120 | }
121 | } else if (Object.hasOwnProperty.call(DELIMITED_BODIES, chr)) {
122 | delimiter = chr;
123 | ++nConsumed;
124 | } else {
125 | throw new Error(
126 | errorMessage = msg`Expected delimiter at ${ text }`);
127 | }
128 | }
129 | text = text.substring(nConsumed);
130 | }
131 | return delimiter;
132 | };
133 | }
134 |
--------------------------------------------------------------------------------
/lib/pg-escaper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | /* eslint id-length: 0, complexity: ["error", { "max": 15 }] */
19 |
20 | 'use strict';
21 |
22 | const {
23 | CHARS_GLOBAL_REGEXP,
24 | escapeSeries,
25 | isSeries,
26 | isSqlFragment,
27 | makeEscaper,
28 | } = require('./escapers.js');
29 |
30 | const { toString: bufferProtoToString } = Buffer.prototype;
31 | const { isBuffer } = Buffer;
32 | const { apply } = Reflect;
33 |
34 | const QUAL_GLOBAL_REGEXP = /\./g;
35 | const PG_ID_REGEXP = /^(?:"(?:[^"]|"")+"|u&"(?:[^"\\]|""|\\.)+")$/i;
36 | const PG_QUAL_ID_REGEXP = /^(?:(?:"(?:[^"]|"")+"|u&"(?:[^"\\]|""|\\.)+")(?:[.](?!$)|$))+$/;
37 |
38 | // Note: NULs are not allowed in text data value.
39 | // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE says
40 | // "The character with the code zero cannot be in a string constant."
41 | // Similarly
42 | // "Quoted identifiers can contain any character, except the character with code zero."
43 |
44 | const PG_CHARS_ESCAPE_MAP = {
45 | __proto__: null,
46 | // See note on NUL above
47 | '\0': '',
48 | '\b': '\b',
49 | '\t': '\t',
50 | '\n': '\n',
51 | '\r': '\r',
52 | '\x1a': '\x1a',
53 | '"': '"',
54 | '$': '$',
55 | '\'': '\'\'',
56 | '\\': '\\',
57 | };
58 |
59 | const PG_ID_ESCAPE_MAP = {
60 | __proto__: null,
61 | // See note on NUL above
62 | '\0': '',
63 | '\b': '\b',
64 | '\t': '\t',
65 | '\n': '\n',
66 | '\r': '\r',
67 | '\x1a': '\x1a',
68 | '"': '""',
69 | '$': '$',
70 | '\'': '\'',
71 | '\\': '\\',
72 | };
73 |
74 | const PG_E_CHARS_ESCAPE_MAP = {
75 | __proto__: null,
76 | // See note on NUL above
77 | '\0': '',
78 | '\b': '\\b',
79 | '\t': '\\t',
80 | '\n': '\\n',
81 | '\r': '\\r',
82 | '\x1a': '\\x1a',
83 | '"': '\\"',
84 | '$': '\\$',
85 | // This fails safe when we pick the wrong escaping convention for a
86 | // single-quote delimited string.
87 | // Empirically, from a psql10 client,
88 | // # SELECT e'foo''bar';
89 | // ?column?
90 | // ----------
91 | // foo'bar
92 | '\'': '\'\'',
93 | '\\': '\\\\',
94 | };
95 |
96 | const PG_U_CHARS_ESCAPE_MAP = {
97 | __proto__: null,
98 | // See note on NUL above
99 | '\0': '',
100 | '\b': '\\0008',
101 | '\t': '\\0009',
102 | '\n': '\\000a',
103 | '\r': '\\000d',
104 | '\x1a': '\\001a',
105 | '"': '\\0022',
106 | '$': '\\0024',
107 | '\'': '\\0027',
108 | '\\': '\\005c',
109 | };
110 |
111 | const HEX_GLOBAL_REGEXP = /[0-9A-Fa-f]/g;
112 | const HEX_TO_BINARY_TABLE = {
113 | __proto__: null,
114 | '0': '0000',
115 | '1': '0001',
116 | '2': '0010',
117 | '3': '0011',
118 | '4': '0100',
119 | '5': '0101',
120 | '6': '0110',
121 | '7': '0111',
122 | '8': '1000',
123 | '9': '1001',
124 | 'A': '1010',
125 | 'B': '1011',
126 | 'C': '1100',
127 | 'D': '1101',
128 | 'E': '1110',
129 | 'F': '1111',
130 | 'a': '1010',
131 | 'b': '1011',
132 | 'c': '1100',
133 | 'd': '1101',
134 | 'e': '1110',
135 | 'f': '1111',
136 | };
137 |
138 | function hexDigitToBinary(digit) {
139 | return HEX_TO_BINARY_TABLE[digit];
140 | }
141 |
142 | function hexToBinary(str) {
143 | return str.replace(HEX_GLOBAL_REGEXP, hexDigitToBinary);
144 | }
145 |
146 | function pgEscapeStringBody(str, escapeMap) {
147 | let chunkIndex = 0;
148 | let escapedVal = '';
149 |
150 | CHARS_GLOBAL_REGEXP.lastIndex = 0;
151 | for (let match; (match = CHARS_GLOBAL_REGEXP.exec(str));) {
152 | escapedVal += str.substring(chunkIndex, match.index) + escapeMap[match[0]];
153 | chunkIndex = CHARS_GLOBAL_REGEXP.lastIndex;
154 | }
155 |
156 | if (chunkIndex === 0) {
157 | // Nothing was escaped
158 | return str;
159 | }
160 |
161 | if (chunkIndex < str.length) {
162 | escapedVal += str.substring(chunkIndex);
163 | }
164 |
165 | return escapedVal;
166 | }
167 |
168 | function pgEscapeId(val, forbidQualified, unicode) {
169 | if (isSqlFragment(val)) {
170 | const { content } = val;
171 | if ((forbidQualified ? PG_ID_REGEXP : PG_QUAL_ID_REGEXP).test(content)) {
172 | return content;
173 | }
174 | throw new Error(`Expected id, got ${ content }`);
175 | }
176 | if (isSeries(val)) {
177 | return escapeSeries(val, (element) => pgEscapeId(element, forbidQualified, unicode), false);
178 | }
179 | let escaped = unicode ?
180 | pgEscapeStringBody(`${ val }`, PG_U_CHARS_ESCAPE_MAP) :
181 | pgEscapeStringBody(`${ val }`, PG_ID_ESCAPE_MAP);
182 | if (!forbidQualified) {
183 | escaped = escaped.replace(QUAL_GLOBAL_REGEXP, unicode ? '".u&"' : '"."');
184 | }
185 | return `${ unicode ? 'u&"' : '"' }${ escaped }"`;
186 | }
187 |
188 | const PG_ID_DELIMS_REGEXP = /^(?:[Uu]&)?"|"$/g;
189 |
190 | function pgEscapeString(val) {
191 | const str = `${ val }`;
192 |
193 | const escapedVal = pgEscapeStringBody(val, PG_E_CHARS_ESCAPE_MAP);
194 |
195 | if (escapedVal === str) {
196 | return `'${ escapedVal }'`;
197 | }
198 |
199 | // If there are any backslashes or quotes, we use e'...' style strings since
200 | // those allow a consistent scheme for escaping all string meta-characters so entail
201 | // the fewest assumptions.
202 | return `e'${ escapedVal }'`;
203 | }
204 |
205 | const pgEscape = makeEscaper(pgEscapeId, pgEscapeString);
206 |
207 | function pgEscapeDelimitedString(strValue, delimiter) {
208 | switch (delimiter) {
209 | case '\'':
210 | case 'b\'':
211 | case 'x\'':
212 | return pgEscapeStringBody(strValue, PG_CHARS_ESCAPE_MAP);
213 | case 'e\'':
214 | return pgEscapeStringBody(strValue, PG_E_CHARS_ESCAPE_MAP);
215 | case 'e':
216 | return `'${ pgEscapeStringBody(strValue, PG_E_CHARS_ESCAPE_MAP) }'`;
217 | case 'u&\'':
218 | return pgEscapeStringBody(strValue, PG_U_CHARS_ESCAPE_MAP);
219 | default:
220 | break;
221 | }
222 |
223 | if (delimiter[0] === '$' && delimiter.indexOf('$', 1) === delimiter.length - 1) {
224 | // Handle literal strings like $tag$...$tag$
225 | let embedHazard = strValue.indexOf(delimiter) >= 0;
226 | if (!embedHazard) {
227 | const lastDollar = strValue.lastIndexOf('$');
228 | if (lastDollar >= 0) {
229 | const tail = strValue.substring(lastDollar);
230 | embedHazard = (tail === delimiter.substring(0, tail.length));
231 | }
232 | }
233 | if (embedHazard) {
234 | throw new Error(`Cannot embed ${ JSON.stringify(strValue) } between ${ delimiter }`);
235 | }
236 | return strValue;
237 | }
238 | throw new Error(`Cannot escape with ${ delimiter }`);
239 | }
240 |
241 | function pgEscapeDelimited(value, delimiter, timeZone, forbidQualified) {
242 | if (delimiter === '"') {
243 | return pgEscapeId(value, forbidQualified, false).replace(PG_ID_DELIMS_REGEXP, '');
244 | } else if (delimiter === 'u&"') {
245 | return pgEscapeId(value, forbidQualified, true).replace(PG_ID_DELIMS_REGEXP, '');
246 | }
247 |
248 | let strValue = value;
249 | if (isBuffer(value)) {
250 | const wantsBinaryDigits = delimiter === 'b\'';
251 | const encoding = wantsBinaryDigits || delimiter === 'x\'' ? 'hex' : 'binary';
252 | strValue = apply(bufferProtoToString, value, [ encoding ]);
253 | if (wantsBinaryDigits) {
254 | // encoding='binary' to buffer means something very different from binary
255 | // encoding in PGSql.
256 | strValue = hexToBinary(strValue);
257 | }
258 | }
259 | return pgEscapeDelimitedString(`${ strValue }`, delimiter);
260 | }
261 |
262 | module.exports = Object.freeze({
263 | escape: pgEscape,
264 | escapeId: pgEscapeId,
265 | escapeDelimited: pgEscapeDelimited,
266 | });
267 |
--------------------------------------------------------------------------------
/lib/pg-lexer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | 'use strict';
19 |
20 | // A simple lexer for Postgres SQL.
21 | //
22 | // https://www.postgresql.org/docs/9.0/static/sql-syntax-lexical.html
23 | //
24 | // -- line chars line comment
25 | // /* block */ block comment. may nest: /* /* */ still in comment */
26 | //
27 | // "..." identifier literal
28 | // U&"..." identifier literal with unicode escapes
29 | // UESCAPE symbol may follow U& string to override \ as escape character
30 | //
31 | // '...' string literal
32 | // E'...' supports C-style escape sequences
33 | // U&'...' string literal with unicode escapes
34 | // UESCAPE symbol ditto
35 | // B'...' binary literal
36 | // X'...' hex literal
37 | //
38 | // $$...$$ string literal with no escaping convention
39 | // $foo$...$foo$ string literal where "foo" may be any run of identifier chars
40 |
41 |
42 | // eslint-disable-next-line no-use-before-define
43 | exports.makeLexer = makeLexer;
44 |
45 |
46 | const TOP_LEVEL_DELIMITER = new RegExp(
47 | // Line comment
48 | '--' +
49 | // or a block comment start
50 | '|/[*]' +
51 | // or an unescaped string start
52 | // Tag has the form of an unquoted identifier without embedded '$'.
53 | // TODO: should allow non-ascii identifiers. Might need to normalize.
54 | '|[$](?:[a-zA-Z_][a-zA-Z_0-9]*)?[$]' +
55 | // or an identifier start
56 | '|(?:[Uu]&)?"' +
57 | // or an escaped string start
58 | '|(?:[Uu]&|[EeBbXx])?\'');
59 |
60 | const LINE_COMMENT_BODY = /^[^\r\n]*/;
61 |
62 | const BLOCK_COMMENT_TOKEN = /[*][/]|[/][*]/;
63 |
64 | const ESC_DQ_STRING_BODY = /^(?:[^"\\]|""|\\.)*(")?/;
65 | const ESC_SQ_STRING_BODY = /^(?:[^'\\]|''|\\.)*(')?/;
66 |
67 | const SIMPLE_DQ_STRING_BODY = /^(?:[^"]|"")*(")?/;
68 | const SIMPLE_SQ_STRING_BODY = /^(?:[^']|'')*(')?/;
69 |
70 | const ESC_STRING_CONTINUATION = /^[\t\n\r ]*([/][*]|--|')?/;
71 |
72 | const STRING_BODIES = {
73 | __proto__: null,
74 | '"': SIMPLE_DQ_STRING_BODY,
75 | 'u&"': ESC_DQ_STRING_BODY,
76 | '\'': SIMPLE_SQ_STRING_BODY,
77 | 'b\'': SIMPLE_SQ_STRING_BODY,
78 | 'e\'': ESC_SQ_STRING_BODY,
79 | 'u&\'': ESC_SQ_STRING_BODY,
80 | 'x\'': SIMPLE_SQ_STRING_BODY,
81 | };
82 |
83 | const LAST_DELIMITER_CHARACTER_TO_HANDLER = {
84 | '-': (delimiter, chunk) => {
85 | // delimiter is --
86 | const match = LINE_COMMENT_BODY.exec(chunk);
87 | const remainder = chunk.substring(match[0].length);
88 | if (remainder) {
89 | return [ null, remainder ];
90 | }
91 | throw new Error(`Unterminated line comment: --${ chunk }`);
92 | },
93 | '*': (delimiter, chunk) => {
94 | // delimiter is '/*'.
95 | let depth = delimiter.length / 2;
96 | let remainder = chunk;
97 | while (remainder) {
98 | const match = BLOCK_COMMENT_TOKEN.exec(remainder);
99 | if (!match) {
100 | break;
101 | }
102 | remainder = remainder.substring(match.index + 2);
103 | if (match[0] === '/*') {
104 | ++depth;
105 | } else {
106 | // */
107 | --depth;
108 | if (!depth) {
109 | break;
110 | }
111 | }
112 | }
113 | if (depth) {
114 | throw new Error(`Unterminated block comment: /*${ chunk }`);
115 | }
116 | return [ null, remainder ];
117 |
118 | // TODO: Do we need to take into account nested "--".
119 | // soc.if.usp.br/manual/postgresql-doc-7.4/html/plpgsql-structure.html says
120 | // "double dash comments can be enclosed into a block comment and
121 | // a double dash can hide the block comment delimiters /* and */."
122 | },
123 | '"': (delimiter, chunk) => {
124 | const match = STRING_BODIES[delimiter].exec(chunk);
125 | const remainder = chunk.substring(match[0].length);
126 | if (match[1]) {
127 | return [ null, remainder ];
128 | }
129 | if (match[0]) {
130 | return [ delimiter, remainder ];
131 | }
132 | throw new Error(`Incomplete escape sequence in ${ delimiter } delimited string at \`${ chunk }\``);
133 | },
134 | '\'': (delimiter, chunk) => {
135 | const match = STRING_BODIES[delimiter].exec(chunk);
136 | const remainder = chunk.substring(match[0].length);
137 | if (match[1]) {
138 | return [
139 | // 4.1.2.2. String Constants with C-style Escapes
140 | // (When continuing an escape string constant across lines,
141 | // write E only before the first opening quote.)
142 | (delimiter === 'e\'' || delimiter === 'E\'') ? 'e' : null, // eslint-disable-line array-element-newline
143 | remainder,
144 | ];
145 | }
146 | if (match[0]) {
147 | return [ delimiter, remainder ];
148 | }
149 | throw new Error(`Incomplete escape sequence in ${ delimiter } delimited string at \`${ chunk }\``);
150 | },
151 | '$': (delimiter, chunk) => {
152 | // TODO: should this match be case insensitive? $x$...$X$
153 | const i = chunk.indexOf(delimiter);
154 | if (i >= 0) {
155 | return [ null, chunk.substring(i + delimiter.length) ];
156 | }
157 | const lastDollar = chunk.lastIndexOf('$');
158 | if (lastDollar >= 0) {
159 | const suffix = chunk.substring(lastDollar);
160 | if (delimiter.indexOf(suffix) === 0) {
161 | // merge hazard
162 | throw new Error(`merge hazard '${ suffix }' at end of ${ delimiter } delimited string`);
163 | }
164 | }
165 | return [ delimiter, '' ];
166 | },
167 | // Special handler to detect e'...' continuations. See 'e' case above.
168 | 'e': (delimiter, chunk) => {
169 | let remainder = chunk;
170 | while (remainder) {
171 | const match = ESC_STRING_CONTINUATION.exec(remainder);
172 | let [ consumed, subdelim ] = match; // eslint-disable-line prefer-const
173 | if (!consumed) {
174 | return [ null, remainder ];
175 | }
176 | remainder = remainder.substring(consumed.length);
177 | if (subdelim) {
178 | if (subdelim === '\'') {
179 | return [ 'e\'', remainder ];
180 | }
181 | while (remainder && subdelim) {
182 | const handler = LAST_DELIMITER_CHARACTER_TO_HANDLER[subdelim[subdelim.length - 1]];
183 | [ subdelim, remainder ] = handler(subdelim, remainder);
184 | }
185 | }
186 | }
187 | return [ delimiter, remainder ];
188 | },
189 | };
190 |
191 | function replayError(fun) {
192 | let message = null;
193 | return (...args) => {
194 | if (message !== null) {
195 | throw new Error(message);
196 | }
197 | try {
198 | return fun(...args);
199 | } catch (exc) {
200 | message = `${ exc.message }`;
201 | throw exc;
202 | }
203 | };
204 | }
205 |
206 | function makeLexer() {
207 | let delimiter = null;
208 | let continuationAmbiguity = false;
209 | let chunkIndex = -1;
210 |
211 | function consumeFromLeft(remainder) {
212 | if (delimiter) {
213 | const lastChar = delimiter[delimiter.length - 1];
214 | if (lastChar !== '*' && lastChar !== '-') {
215 | continuationAmbiguity = false;
216 | }
217 | const handler = LAST_DELIMITER_CHARACTER_TO_HANDLER[lastChar];
218 | ([ delimiter, remainder ] = handler(delimiter, remainder));
219 | } else {
220 | const match = TOP_LEVEL_DELIMITER.exec(remainder);
221 | if (continuationAmbiguity) {
222 | const end = match ? match.index : remainder.length;
223 | if (/[^\t\n\r ]/.test(remainder.substring(0, end))) {
224 | continuationAmbiguity = false;
225 | }
226 | }
227 | if (!match) {
228 | return '';
229 | }
230 | [ delimiter ] = match;
231 | if (delimiter[0] !== '$') {
232 | // Empirically,
233 | // postgres=# SELECT $foo$bar$Foo$;
234 | // postgres$# $foo$;
235 | // ?column?
236 | // -----------
237 | // bar$Foo$;+
238 | delimiter = delimiter.toLowerCase();
239 | }
240 | remainder = remainder.substring(match.index + delimiter.length);
241 | }
242 | return remainder;
243 | }
244 |
245 | function lexer(chunk) {
246 | if (chunk === null) {
247 | if (delimiter && delimiter !== 'e') {
248 | throw new Error(`Unclosed quoted string: ${ delimiter }`);
249 | }
250 | return delimiter;
251 | }
252 |
253 | ++chunkIndex;
254 |
255 | if (continuationAmbiguity && chunkIndex > 1) {
256 | // If any chunk besides the last contains a newline and
257 | // does not contain any non-whitespace or comment content,
258 | // then we have a continuation ambiguity.
259 | //
260 | // For example,
261 | // pg`SELECT ${ x }
262 | // ${ y }`
263 | // then we would have to know how ${ x } was escaped to
264 | // determine how to escape ${ y } because of a string
265 | // continuation corner-case:
266 | //
267 | // From https://www.postgresql.org/docs/9.0/static/sql-syntax-lexical.html
268 | // "Two string constants that are only separated by whitespace with at
269 | // least one newline are concatenated and effectively treated as if the
270 | // string had been written as one constant."
271 | //
272 | // "When continuing an escape string constant across lines, write E only
273 | // before the first opening quote."
274 | //
275 | // To decide whether to wrap y using e'...' ${ y } we need to know about
276 | // ${ x }.
277 | throw new Error(
278 | // eslint-disable-next-line no-template-curly-in-string
279 | 'Potential for ambiguous string continuation at `${ chunk }`.' +
280 | ' If you need string continuation start with an e\'...\' string.');
281 | }
282 |
283 | let remainder = `${ chunk }`;
284 | continuationAmbiguity = /[\n\r]/.test(chunk);
285 | while (remainder) {
286 | remainder = consumeFromLeft(remainder);
287 | }
288 | return delimiter;
289 | }
290 |
291 | return replayError(lexer);
292 | }
293 |
294 | module.exports.makeLexer = makeLexer;
295 |
--------------------------------------------------------------------------------
/lib/tag-fn.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | 'use strict';
19 |
20 | const {
21 | memoizedTagFunction,
22 | trimCommonWhitespaceFromLines,
23 | } = require('template-tag-common');
24 |
25 | const LITERAL_BACKTICK_FIXUP_PATTERN = /((?:[^\\]|\\[^`])+)|\\(`)(?!`)/g;
26 |
27 | /**
28 | * Trims common whitespace and converts escaped backticks
29 | * to backticks as appropriate.
30 | *
31 | * @param {!Array.} strings a valid TemplateObject.
32 | * @return {!Array.} the adjusted raw strings.
33 | */
34 | function prepareStrings(strings) {
35 | const raw = trimCommonWhitespaceFromLines(strings).raw.slice();
36 | for (let i = 0, len = raw.length; i < len; ++i) {
37 | // Convert \` to ` but leave \\` alone.
38 | raw[i] = raw[i].replace(LITERAL_BACKTICK_FIXUP_PATTERN, '$1$2');
39 | }
40 | return raw;
41 | }
42 |
43 | /**
44 | * Returns a template tag function that contextually autoescapes values
45 | * producing a SqlFragment.
46 | */
47 | function makeSqlTagFunction(
48 | { makeLexer },
49 | escape,
50 | escapeDelimitedValue,
51 | fixupBackticks,
52 | decorateOutput) {
53 | /**
54 | * Analyzes the static parts of the tag content.
55 | *
56 | * @param {!Array.} strings a valid TemplateObject.
57 | * @return { !{
58 | * delimiters : !Array.,
59 | * chunks: !Array.
60 | * } }
61 | * A record like { delimiters, chunks }
62 | * where delimiter is a contextual cue and chunk is
63 | * the adjusted raw text.
64 | */
65 | function computeStatic(strings) {
66 | const chunks = fixupBackticks ? prepareStrings(strings) : strings.raw;
67 | const lexer = makeLexer();
68 |
69 | const delimiters = [];
70 | for (let i = 0, len = chunks.length; i < len; ++i) {
71 | const chunk = String(chunks[i]);
72 | delimiters.push(lexer(chunk));
73 | }
74 |
75 | // Signal end of input.
76 | lexer(null);
77 |
78 | return { delimiters, chunks };
79 | }
80 |
81 | function defangMergeHazard(before, escaped, after) {
82 | const escapedLast = escaped[escaped.length - 1];
83 | if ('"\'`'.indexOf(escapedLast) < 0) {
84 | // Not a merge hazard.
85 | return escaped;
86 | }
87 |
88 | let escapedSetOff = escaped;
89 | const lastBefore = before[before.length - 1];
90 | if (escapedLast === escaped[0] && escapedLast === lastBefore) {
91 | escapedSetOff = ` ${ escapedSetOff }`;
92 | }
93 | if (escapedLast === after[0]) {
94 | escapedSetOff += ' ';
95 | }
96 | return escapedSetOff;
97 | }
98 |
99 | function interpolateSqlIntoFragment(
100 | { stringifyObjects, timeZone, forbidQualified },
101 | { delimiters, chunks },
102 | strings, values) {
103 | // A buffer to accumulate output.
104 | let [ result ] = chunks;
105 | for (let i = 1, len = chunks.length; i < len; ++i) {
106 | const chunk = chunks[i];
107 | // The count of values must be 1 less than the surrounding
108 | // chunks of literal text.
109 | const delimiter = delimiters[i - 1];
110 | const value = values[i - 1];
111 |
112 | const escaped = delimiter ?
113 | escapeDelimitedValue(value, delimiter, timeZone, forbidQualified) :
114 | defangMergeHazard(
115 | result,
116 | escape(value, stringifyObjects, timeZone),
117 | chunk);
118 |
119 | result += escaped + chunk;
120 | }
121 |
122 | return decorateOutput(result);
123 | }
124 |
125 | return memoizedTagFunction(computeStatic, interpolateSqlIntoFragment);
126 | }
127 |
128 | module.exports.makeSqlTagFunction = makeSqlTagFunction;
129 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "safesql",
3 | "description": "string template tags for safely composing MySQL and PostgreSQL query strings",
4 | "keywords": [
5 | "sql",
6 | "security",
7 | "injection",
8 | "template",
9 | "template-tag",
10 | "string-template",
11 | "sec-roadmap",
12 | "es6"
13 | ],
14 | "version": "2.0.2",
15 | "main": "index.js",
16 | "files": [
17 | "fragment.js",
18 | "id.js",
19 | "index.js",
20 | "lib/*.js"
21 | ],
22 | "mintable": {
23 | "selfNominate": [
24 | "safesql/fragment",
25 | "safesql/id"
26 | ]
27 | },
28 | "scripts": {
29 | "cover": "istanbul cover _mocha",
30 | "coveralls": "npm run cover -- --report lcovonly && cat ./coverage/lcov.info | coveralls",
31 | "lint": "./node_modules/.bin/eslint .",
32 | "prepack": "npm run lint && npm test && ./scripts/make-md-toc.pl README.md",
33 | "test": "mocha"
34 | },
35 | "pre-commit": [
36 | "prepack"
37 | ],
38 | "author": "@mikesamuel",
39 | "license": "Apache-2.0",
40 | "repository": {
41 | "type": "git",
42 | "url": "git+https://github.com/mikesamuel/safesql.git"
43 | },
44 | "bugs": {
45 | "url": "https://github.com/mikesamuel/safesql/issues"
46 | },
47 | "dependencies": {
48 | "template-tag-common": "^5.0.2"
49 | },
50 | "devDependencies": {
51 | "chai": "^4.1.2",
52 | "coveralls": "^3.0.1",
53 | "eslint": "^4.19.1",
54 | "eslint-config-strict": "^14.0.1",
55 | "istanbul": "^0.4.5",
56 | "mocha": "^4.0.1",
57 | "mocha-lcov-reporter": "^1.3.0",
58 | "pre-commit": "^1.2.2"
59 | },
60 | "eslintIgnore": [
61 | "/coverage/**"
62 | ],
63 | "eslintConfig": {
64 | "extends": [
65 | "strict"
66 | ],
67 | "parserOptions": {
68 | "ecmaVersion": 6,
69 | "sourceType": "source",
70 | "ecmaFeatures": {
71 | "impliedStrict": false
72 | }
73 | },
74 | "rules": {
75 | "no-warning-comments": [
76 | "error",
77 | {
78 | "terms": [
79 | "do not submit"
80 | ]
81 | }
82 | ],
83 | "no-void": "off",
84 | "strict": [
85 | "error",
86 | "global"
87 | ]
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/scripts/make-md-toc.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl
2 |
3 | use strict;
4 |
5 | foreach my $path (@ARGV) {
6 | open (my $IN, "<$path") or die "$path: $!";
7 | my %ids = ();
8 | my @toc = ();
9 | my $content = "";
10 |
11 | my $lastDepth = 0;
12 | while (<$IN>) {
13 | if (m/^(\#{2,})(.*?)<\/a>\s*$/) {
14 | my $depth = length($1) - 1;
15 | my $text = $2;
16 | my $id = $3;
17 | if (exists($ids{$id})) {
18 | die "$path:$.: Heading id $id previously seen at $ids{$id}";
19 | } else {
20 | $ids{$id} = $.;
21 | }
22 | if ($depth > $lastDepth + 1) {
23 | die "$path:$.: Heading id $id has depth $depth which skips levels from $lastDepth";
24 | }
25 | $text =~ s/^\s*|\s*$//g;
26 | push(@toc, (" " x ($depth - 1)) . "* [$text](#$id)\n");
27 | $lastDepth = $depth;
28 | } elsif (m/^##/) {
29 | die "$path:$.: Heading lacks identifier";
30 | }
31 | $content .= $_;
32 | }
33 |
34 | close ($IN) or die "$path: $!";
35 |
36 | my $toc = join("", @toc);
37 | unless ($content =~ s/(\n\n).*?(\n\n)/$1\n$toc$2/s) {
38 | die "$path: Cannot find delimited space for the table of contents";
39 | }
40 |
41 | my $outpath = "$path.out";
42 | open (my $OUT, ">$outpath") or die "$path: $!";
43 | print $OUT "$content";
44 | close ($OUT) or die "$path: $!";
45 |
46 | rename($outpath, $path) or die "$path: Failed to rename $outpath to $path $!";
47 | }
48 |
--------------------------------------------------------------------------------
/scripts/prepublish.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | [ -n "$TMPDIR" ]
6 | [ -d "$TMPDIR" ]
7 |
8 | export PROJECT_NAME=$(node -e 'console.log(require("./package.json").name)')
9 | export TMP_WORKSPACE="$TMPDIR"/"$PROJECT_NAME"-test-workspace
10 |
11 | rm -rf "$TMP_WORKSPACE"
12 | mkdir -p "$TMP_WORKSPACE"/package
13 |
14 | # Repack, and check the contents
15 | export TARBALL="$(npm pack 2> /dev/null | tail -1)"
16 |
17 | echo PACKAGE CONTENTS:
18 | tar tfz "$TARBALL"
19 |
20 | read -p 'Does the package contents look ok? (yes|no) ' PACKED_OK
21 |
22 | echo "$PACKED_OK" | egrep -qi '^y'
23 |
24 |
25 | # Test that it installs and tests run in isolation
26 | cp "$TARBALL" "$TMP_WORKSPACE"/
27 | cp -r test/ "$TMP_WORKSPACE"/package/test/
28 | pushd "$TMP_WORKSPACE"
29 | tar xfz "$TARBALL" && (
30 | pushd "$TMP_WORKSPACE"/package
31 | npm install \
32 | && npm run lint \
33 | && npm run cover
34 | popd >& /dev/null
35 | )
36 | popd >& /dev/null
37 |
38 | rm "$TARBALL"
39 |
40 |
41 | echo '
42 | 1. Figure out what kind of release it is:
43 | * patch
44 | * minor
45 | * major
46 |
47 | Assuming it is in `$NPM_VERSION_BUMP`:
48 | $ npm version "$NPM_VERSION_BUMP"
49 |
50 |
51 | 2. Get a 2FA nonce from the Google Authenticator app.
52 | Assuming it is in `$OTP`:
53 | $ npm publish --otp "$OTP"
54 |
55 |
56 | 3. Push the release label to GitHub.
57 | $ git push --tags origin master
58 | '
59 |
--------------------------------------------------------------------------------
/scripts/validate.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | NODE_MAJOR_VERSION="$(node -v | perl -ne 'print $1 if m/^v?(\d+)[.]/')"
6 | [ -n "$NODE_MAJOR_VERSION" ]
7 |
8 | if [[ "$NODE_MAJOR_VERSION" -gt 7 ]]; then
9 | # Standard fails on node 7 when run on travis-ci due to some odd
10 | # interaction between standard and an eslint plugin. We really
11 | # only need to run the linter on one platform.
12 | npm run-script lint
13 | else
14 | echo Skipping linter on node v"$NODE_MAJOR_VERSION"
15 | fi
16 | npm test
17 |
--------------------------------------------------------------------------------
/test/escapers-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | /* eslint "id-length": 0, "id-blacklist": 0, "no-magic-numbers": 0 */
19 |
20 | 'use strict';
21 |
22 | require('module-keys/cjs').polyfill(module, require, 'safesql/test/escaper-test.js');
23 |
24 | const { expect } = require('chai');
25 | const { describe, it } = require('mocha');
26 |
27 | const { mysql, pg } = require('../index.js');
28 | const mysqlEscaper = require('../lib/mysql-escaper.js');
29 | const pgEscaper = require('../lib/pg-escaper.js');
30 | const escapers = {
31 | mysql: mysqlEscaper,
32 | pg: pgEscaper,
33 | };
34 |
35 | describe('escapers', () => {
36 | for (const target of [ 'mysql', 'pg' ]) {
37 | // eslint-disable-next-line no-use-before-define
38 | describe(target, () => testEscapes(target, escapers[target]));
39 | }
40 | });
41 |
42 | function testEscapes(target, { escape, escapeId }) {
43 | describe('escapeId', () => {
44 | it('value is quoted', () => {
45 | expect(escapeId('id')).to.equal(
46 | {
47 | mysql: '`id`',
48 | pg: '"id"',
49 | }[target]);
50 | });
51 |
52 | it('value can be a number', () => {
53 | expect(escapeId(42)).to.equal({
54 | mysql: '`42`',
55 | pg: '"42"',
56 | }[target]);
57 | });
58 |
59 | it('value can be an object', () => {
60 | expect(escapeId({})).to.equal({
61 | mysql: '`[object Object]`',
62 | pg: '"[object Object]"',
63 | }[target]);
64 | });
65 |
66 | it('value toString is called', () => {
67 | expect(escapeId({ toString() {
68 | return 'foo';
69 | } })).to.equal({
70 | mysql: '`foo`',
71 | pg: '"foo"',
72 | }[target]);
73 | });
74 |
75 | it('value toString is quoted', () => {
76 | expect(escapeId({
77 | toString() {
78 | return 'f`"oo';
79 | },
80 | })).to.equal({
81 | mysql: '`f``"oo`',
82 | pg: '"f`""oo"',
83 | }[target]);
84 | });
85 |
86 | it('value containing escapes is quoted', () => {
87 | expect(escapeId('i`"d')).to.equal({
88 | mysql: '`i``"d`',
89 | pg: '"i`""d"',
90 | }[target]);
91 | });
92 |
93 | it('value containing separator is quoted', () => {
94 | expect(escapeId('id1.id2')).to.equal({
95 | mysql: '`id1`.`id2`',
96 | pg: '"id1"."id2"',
97 | }[target]);
98 | });
99 |
100 | it('value containing separator and escapes is quoted', () => {
101 | expect(escapeId('id`1.i"d2')).to.equal({
102 | mysql: '`id``1`.`i"d2`',
103 | pg: '"id`1"."i""d2"',
104 | }[target]);
105 | });
106 |
107 | it('value containing separator is fully escaped when forbidQualified', () => {
108 | expect(escapeId('id1.id2', true)).to.equal({
109 | mysql: '`id1.id2`',
110 | pg: '"id1.id2"',
111 | }[target]);
112 | });
113 |
114 | it('arrays are turned into lists', () => {
115 | expect(escapeId([ 'a', 'b', 't.c' ])).to.equal({
116 | mysql: '`a`, `b`, `t`.`c`',
117 | pg: '"a", "b", "t"."c"',
118 | }[target]);
119 | });
120 |
121 | it('nested arrays are flattened', () => {
122 | expect(escapeId([ 'a', [ 'b', [ 't.c' ] ] ])).to.equal({
123 | mysql: '`a`, `b`, `t`.`c`',
124 | pg: '"a", "b", "t"."c"',
125 | }[target]);
126 | });
127 |
128 | describe('qualified id to escapeId', () => {
129 | const qualifiedId = {
130 | mysql: mysql`\`id1\`.\`id2\``,
131 | pg: pg`"id1"."id2"`,
132 | }[target];
133 | it('rejects', () => {
134 | expect(() => escapeId(qualifiedId, true)).to.throw();
135 | });
136 | it('allow', () => {
137 | expect(() => escapeId(qualifiedId, false)).to.not.throw();
138 | });
139 | });
140 | });
141 |
142 | describe('escape', () => {
143 | it('undefined -> NULL', () => {
144 | expect(escape(void 0)).to.equal('NULL');
145 | });
146 |
147 | it('null -> NULL', () => {
148 | expect(escape(null)).to.equal('NULL');
149 | });
150 |
151 | it('booleans convert to strings', () => {
152 | expect(escape(false)).to.equal('false');
153 | expect(escape(true)).to.equal('true');
154 | });
155 |
156 | it('numbers convert to strings', () => {
157 | expect(escape(5)).to.equal('5');
158 | });
159 |
160 | it('raw not escaped', () => {
161 | expect(escape(mysql`NOW()`)).to.equal('NOW()');
162 | });
163 |
164 | it('objects are turned into key value pairs', () => {
165 | expect(escape({ a: 'b', c: 'd' })).to.equal({
166 | mysql: '`a` = \'b\', `c` = \'d\'',
167 | pg: '"a" = \'b\', "c" = \'d\'',
168 | }[target]);
169 | });
170 |
171 | it('objects function properties are ignored', () => {
172 | // eslint-disable-next-line no-empty-function
173 | expect(escape({ a: 'b', c() {} })).to.equal({
174 | mysql: '`a` = \'b\'',
175 | pg: '"a" = \'b\'',
176 | }[target]);
177 | });
178 |
179 | it('nested toSqlString is not trusted', () => {
180 | expect(escape({ id: { toSqlString() {
181 | return 'LAST_INSERT_ID()';
182 | } } })).to.equal({
183 | mysql: '`id` = \'[object Object]\'',
184 | pg: '"id" = \'[object Object]\'',
185 | }[target]);
186 | });
187 |
188 | it('objects toSqlString is not trusted', () => {
189 | expect(escape({ toSqlString() {
190 | return '@foo_id';
191 | } })).to.equal('');
192 | });
193 |
194 | it('fragment is not quoted', () => {
195 | expect(escape(mysql`CURRENT_TIMESTAMP()`)).to.equal('CURRENT_TIMESTAMP()');
196 | });
197 |
198 | it('nested objects are cast to strings', () => {
199 | expect(escape({ a: { nested: true } })).to.equal({
200 | mysql: '`a` = \'[object Object]\'',
201 | pg: '"a" = \'[object Object]\'',
202 | }[target]);
203 | });
204 |
205 | it('nested objects use toString', () => {
206 | expect(escape({ a: { toString() {
207 | return 'foo';
208 | } } })).to.equal(
209 | {
210 | mysql: '`a` = \'foo\'',
211 | pg: '"a" = \'foo\'',
212 | }[target]);
213 | });
214 |
215 | it('nested objects use toString is quoted', () => {
216 | expect(escape({ a: { toString() {
217 | return 'f\'oo';
218 | } } })).to.equal({
219 | mysql: '`a` = \'f\\\'oo\'',
220 | pg: '"a" = e\'f\'\'oo\'',
221 | }[target]);
222 | });
223 |
224 | it('arrays are turned into lists', () => {
225 | expect(escape([ 1, 2, 'c' ])).to.equal('1, 2, \'c\'');
226 | });
227 |
228 | it('series are turned into lists', () => {
229 | function * items() {
230 | yield 1;
231 | yield 2;
232 | yield 'c';
233 | }
234 | expect(escape(items())).to.equal('1, 2, \'c\'');
235 | });
236 |
237 | it('nested arrays are turned into grouped lists', () => {
238 | function * items() {
239 | yield [ 1, 2, 3 ];
240 | yield (
241 | function * nested() {
242 | yield 4;
243 | yield 5;
244 | yield 6;
245 | }());
246 | yield [ 'a', 'b', { nested: true } ];
247 | }
248 |
249 | expect(escape(items())).to.equal('(1, 2, 3), (4, 5, 6), (\'a\', \'b\', \'[object Object]\')');
250 | });
251 |
252 | it('nested series are turned into grouped lists', () => {
253 | function * items() {
254 | yield 4;
255 | yield 5;
256 | yield 6;
257 | }
258 | expect(escape([ [ 1, 2, 3 ], items(), [ 'a', 'b', { nested: true } ] ]))
259 | .to.equal('(1, 2, 3), (4, 5, 6), (\'a\', \'b\', \'[object Object]\')');
260 | });
261 |
262 | it('nested objects inside arrays are cast to strings', () => {
263 | expect(escape([ 1, { nested: true }, 2 ])).to.equal('1, \'[object Object]\', 2');
264 | });
265 |
266 | it('nested objects inside arrays use toString', () => {
267 | expect(escape([
268 | 1,
269 | { toString() {
270 | return 'foo';
271 | } },
272 | 2,
273 | ])).to.equal('1, \'foo\', 2');
274 | });
275 |
276 | it('strings are quoted', () => {
277 | expect(escape('Super')).to.equal('\'Super\'');
278 | });
279 |
280 | it('\\0 gets escaped', () => {
281 | expect(escape('Sup\u0000er')).to.equal({
282 | mysql: '\'Sup\\0er\'',
283 | pg: 'e\'Super\'',
284 | }[target]);
285 |
286 | expect(escape('Super\u0000')).to.equal({
287 | mysql: '\'Super\\0\'',
288 | pg: 'e\'Super\'',
289 | }[target]);
290 |
291 | expect(escape('Super\u000012')).to.equal({
292 | mysql: '\'Super\\012\'',
293 | pg: 'e\'Super12\'',
294 | }[target]);
295 | });
296 |
297 | it('\\b gets escaped', () => {
298 | expect(escape('Sup\ber')).to.equal({
299 | mysql: '\'Sup\\ber\'',
300 | pg: 'e\'Sup\\ber\'',
301 | }[target]);
302 |
303 | expect(escape('Super\b')).to.equal({
304 | mysql: '\'Super\\b\'',
305 | pg: 'e\'Super\\b\'',
306 | }[target]);
307 | });
308 |
309 | it('\\n gets escaped', () => {
310 | expect(escape('Sup\ner')).to.equal({
311 | mysql: '\'Sup\\ner\'',
312 | pg: 'e\'Sup\\ner\'',
313 | }[target]);
314 |
315 | expect(escape('Super\n')).to.equal({
316 | mysql: '\'Super\\n\'',
317 | pg: 'e\'Super\\n\'',
318 | }[target]);
319 | });
320 |
321 | it('\\r gets escaped', () => {
322 | expect(escape('Sup\rer')).to.equal({
323 | mysql: '\'Sup\\rer\'',
324 | pg: 'e\'Sup\\rer\'',
325 | }[target]);
326 |
327 | expect(escape('Super\r')).to.equal({
328 | mysql: '\'Super\\r\'',
329 | pg: 'e\'Super\\r\'',
330 | }[target]);
331 | });
332 |
333 | it('\\t gets escaped', () => {
334 | expect(escape('Sup\ter')).to.equal({
335 | mysql: '\'Sup\\ter\'',
336 | pg: 'e\'Sup\\ter\'',
337 | }[target]);
338 |
339 | expect(escape('Super\t')).to.equal({
340 | mysql: '\'Super\\t\'',
341 | pg: 'e\'Super\\t\'',
342 | }[target]);
343 | });
344 |
345 | it('\\ gets escaped', () => {
346 | expect(escape('Sup\\er')).to.equal({
347 | mysql: '\'Sup\\\\er\'',
348 | pg: 'e\'Sup\\\\er\'',
349 | }[target]);
350 |
351 | expect(escape('Super\\')).to.equal({
352 | mysql: '\'Super\\\\\'',
353 | pg: 'e\'Super\\\\\'',
354 | }[target]);
355 | });
356 |
357 | it('\\u001a (ascii 26 - Windows EOF) gets replaced', () => {
358 | expect(escape('Sup\u001aer')).to.equal({
359 | mysql: '\'Sup\\Zer\'',
360 | pg: 'e\'Sup\\x1aer\'',
361 | }[target]);
362 |
363 | expect(escape('Super\u001a')).to.equal({
364 | mysql: '\'Super\\Z\'',
365 | pg: 'e\'Super\\x1a\'',
366 | }[target]);
367 | });
368 |
369 | it('single quotes get escaped', () => {
370 | expect(escape('Sup\'er')).to.equal({
371 | mysql: '\'Sup\\\'er\'',
372 | pg: 'e\'Sup\'\'er\'',
373 | }[target]);
374 |
375 | expect(escape('Super\'')).to.equal({
376 | mysql: '\'Super\\\'\'',
377 | pg: 'e\'Super\'\'\'',
378 | }[target]);
379 | });
380 |
381 | it('double quotes get escaped', () => {
382 | expect(escape('Sup"er')).to.equal({
383 | mysql: '\'Sup\\"er\'',
384 | pg: 'e\'Sup\\"er\'',
385 | }[target]);
386 |
387 | expect(escape('Super"')).to.equal({
388 | mysql: '\'Super\\"\'',
389 | pg: 'e\'Super\\"\'',
390 | }[target]);
391 | });
392 |
393 | it('dollar signs get escaped', () => {
394 | expect(escape('foo$$; DELETE')).to.equal({
395 | mysql: String.raw`'foo\$\$; DELETE'`,
396 | pg: String.raw`e'foo\$\$; DELETE'`,
397 | }[target]);
398 | });
399 |
400 | it('dates are converted to YYYY-MM-DD HH:II:SS.sss', () => {
401 | const expected = '2012-05-07 11:42:03.002';
402 | const date = new Date(2012, 4, 7, 11, 42, 3, 2);
403 | const string = escape(date);
404 |
405 | expect(string).to.equal(`'${ expected }'`);
406 | });
407 |
408 | it('dates are converted to specified time zone "Z"', () => {
409 | const expected = '2012-05-07 11:42:03.002';
410 | const date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2));
411 | const string = escape(date, false, 'Z');
412 |
413 | expect(string).to.equal(`'${ expected }'`);
414 | });
415 |
416 | it('dates are converted to specified time zone "+01"', () => {
417 | const expected = '2012-05-07 12:42:03.002';
418 | const date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2));
419 | const string = escape(date, false, '+01');
420 |
421 | expect(string).to.equal(`'${ expected }'`);
422 | });
423 |
424 | it('dates are converted to specified time zone "+0200"', () => {
425 | const expected = '2012-05-07 13:42:03.002';
426 | const date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2));
427 | const string = escape(date, false, '+0200');
428 |
429 | expect(string).to.equal(`'${ expected }'`);
430 | });
431 |
432 | it('dates are converted to specified time zone "-05:00"', () => {
433 | const expected = '2012-05-07 06:42:03.002';
434 | const date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2));
435 | const string = escape(date, false, '-05:00');
436 |
437 | expect(string).to.equal(`'${ expected }'`);
438 | });
439 |
440 | it('dates are converted to UTC for unknown time zone', () => {
441 | const date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2));
442 | const expected = escape(date, false, 'Z');
443 | const string = escape(date, false, 'foo');
444 |
445 | expect(string).to.equal(expected);
446 | });
447 |
448 | it('invalid dates are converted to null', () => {
449 | const date = new Date(NaN);
450 | const string = escape(date);
451 |
452 | expect(string).to.equal('NULL');
453 | });
454 |
455 | it('buffers are converted to hex', () => {
456 | const buffer = Buffer.from([ 0, 1, 254, 255 ]);
457 | const string = escape(buffer);
458 |
459 | expect(string).to.equal('X\'0001feff\'');
460 | });
461 |
462 | it('buffers object cannot inject SQL', () => {
463 | const buffer = Buffer.from([ 0, 1, 254, 255 ]);
464 | buffer.toString = () => '00\' OR \'1\'=\'1';
465 | const string = escape(buffer);
466 |
467 | expect(string).to.equal('X\'0001feff\'');
468 | });
469 |
470 | it('NaN -> NaN', () => {
471 | expect(escape(NaN)).to.equal('NaN');
472 | });
473 |
474 | it('Infinity -> Infinity', () => {
475 | expect(escape(Infinity)).to.equal('Infinity');
476 | });
477 | });
478 | }
479 |
480 | describe('all delimiters', () => {
481 | const { escapeDelimited } = escapers.pg;
482 | it('ok', () => {
483 | for (const okDelim of [ '\'', '"', 'u&"', 'u&\'', 'b\'', 'x\'', 'e\'', '$$', '$foo$' ]) {
484 | expect(() => escapeDelimited('x', okDelim, null, false)).to.not.throw();
485 | }
486 | });
487 | it('bad', () => {
488 | for (const badDelim of [ '', '?', '$x', '$$$', 'z\'' ]) {
489 | expect(() => escapeDelimited('x', badDelim, null, false)).to.throw(Error, 'Cannot escape');
490 | }
491 | });
492 | });
493 |
--------------------------------------------------------------------------------
/test/example-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | /* eslint "id-length": 0, "id-blacklist": 0 */
19 |
20 | 'use strict';
21 |
22 | const { expect } = require('chai');
23 | const { describe, it } = require('mocha');
24 |
25 | const { mysql, pg } = require('../index.js');
26 |
27 | describe('example code', () => {
28 | describe('README.md', () => {
29 | // These mirror example code in ../README.md so if you modify this,
30 | // be sure to reflect changes there.
31 |
32 | describe('SELECT various', () => {
33 | it('mysql', () => {
34 | const table = 'table';
35 | const ids = [ 'x', 'y', 'z' ];
36 | const str = 'foo\'"bar';
37 |
38 | const query = mysql`SELECT * FROM \`${ table }\` WHERE id IN (${ ids }) AND s=${ str }`;
39 |
40 | expect(query.content).to.equal(
41 | 'SELECT * FROM `table` WHERE id IN (\'x\', \'y\', \'z\') AND s=\'foo\\\'\\"bar\'');
42 | });
43 | it('pg', () => {
44 | const table = 'table';
45 | const ids = [ 'x', 'y', 'z' ];
46 | const str = 'foo\'"bar';
47 |
48 | const query = pg`SELECT * FROM "${ table }" WHERE id IN (${ ids }) AND s=${ str }`;
49 |
50 | expect(query.content).to.equal(
51 | String.raw`SELECT * FROM "table" WHERE id IN ('x', 'y', 'z') AND s=e'foo''\"bar'`);
52 | });
53 | });
54 | it('UPDATE obj', () => {
55 | const column = 'users';
56 | const userId = 1;
57 | const data = {
58 | email: 'foobar@example.com',
59 | modified: mysql`NOW()`,
60 | };
61 | const query = mysql`UPDATE \`${ column }\` SET ${ data } WHERE \`id\` = ${ userId }`;
62 |
63 | expect(query.content).to.equal(
64 | 'UPDATE `users` SET `email` = \'foobar@example.com\', `modified` = NOW() WHERE `id` = 1');
65 | });
66 | it('chains', () => {
67 | const data = { a: 1 };
68 | const whereClause = mysql`WHERE ${ data }`;
69 | expect(mysql`SELECT * FROM TABLE ${ whereClause }`.content).to.equal(
70 | 'SELECT * FROM TABLE WHERE `a` = 1');
71 | });
72 | it('no excess quotes', () => {
73 | expect(mysql`SELECT '${ 'foo' }' `.content).to.equal('SELECT \'foo\' ');
74 | expect(mysql`SELECT ${ 'foo' } `.content).to.equal('SELECT \'foo\' ');
75 | });
76 | it('backtick delimited', () => {
77 | expect(mysql`SELECT \`${ 'id' }\` FROM \`TABLE\``.content).to.equal(
78 | 'SELECT `id` FROM `TABLE`');
79 | });
80 | it('raw escapes', () => {
81 | expect(mysql`SELECT "\n"`.content)
82 | .to.equal(String.raw`SELECT "\n"`);
83 | });
84 | it('dates', () => {
85 | const timeZone = 'GMT';
86 | const date = new Date(Date.UTC(2000, 0, 1)); // eslint-disable-line no-magic-numbers
87 | expect(mysql({ timeZone })`SELECT ${ date }`.content)
88 | .to.equal('SELECT \'2000-01-01 00:00:00.000\'');
89 | });
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/test/mysql-lexer-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | 'use strict';
19 |
20 | const { expect } = require('chai');
21 | const { describe, it } = require('mocha');
22 | const mysqlLexer = require('../lib/mysql-lexer.js');
23 |
24 | function tokens(...chunks) {
25 | const { makeLexer } = mysqlLexer;
26 | const lexer = makeLexer();
27 | const out = [];
28 | for (let i = 0, len = chunks.length; i < len; ++i) {
29 | out.push(lexer(chunks[i]) || '_');
30 | }
31 | return out.join(',');
32 | }
33 |
34 | describe('mysql template lexer', () => {
35 | it('empty string', () => {
36 | expect(tokens('')).to.equal('_');
37 | });
38 | it('hash comments', () => {
39 | expect(tokens(' # "foo\n', '')).to.equal('_,_');
40 | });
41 | it('dash comments', () => {
42 | expect(tokens(' -- \'foo\n', '')).to.equal('_,_');
43 | });
44 | it('dash dash participates in number literal', () => {
45 | expect(tokens('SELECT (1--1) + "', '"')).to.equal('",_');
46 | });
47 | it('block comments', () => {
48 | expect(tokens(' /* `foo */', '')).to.equal('_,_');
49 | });
50 | it('dq', () => {
51 | expect(tokens('SELECT "foo"')).to.equal('_');
52 | expect(tokens('SELECT `foo`, "foo"')).to.equal('_');
53 | expect(tokens('SELECT "', '"')).to.equal('",_');
54 | expect(tokens('SELECT "x', '"')).to.equal('",_');
55 | expect(tokens('SELECT "\'', '"')).to.equal('",_');
56 | expect(tokens('SELECT "`', '"')).to.equal('",_');
57 | expect(tokens('SELECT """', '"')).to.equal('",_');
58 | expect(tokens('SELECT "\\"', '"')).to.equal('",_');
59 | });
60 | it('sq', () => {
61 | expect(tokens('SELECT \'foo\'')).to.equal('_');
62 | expect(tokens('SELECT `foo`, \'foo\'')).to.equal('_');
63 | expect(tokens('SELECT \'', '\'')).to.equal('\',_');
64 | expect(tokens('SELECT \'x', '\'')).to.equal('\',_');
65 | expect(tokens('SELECT \'"', '\'')).to.equal('\',_');
66 | expect(tokens('SELECT \'`', '\'')).to.equal('\',_');
67 | expect(tokens('SELECT \'\'\'', '\'')).to.equal('\',_');
68 | expect(tokens('SELECT \'\\\'', '\'')).to.equal('\',_');
69 | });
70 | it('bq', () => {
71 | expect(tokens('SELECT `foo`')).to.equal('_');
72 | expect(tokens('SELECT "foo", `foo`')).to.equal('_');
73 | expect(tokens('SELECT `', '`')).to.equal('`,_');
74 | expect(tokens('SELECT `x', '`')).to.equal('`,_');
75 | expect(tokens('SELECT `\'', '`')).to.equal('`,_');
76 | expect(tokens('SELECT `"', '`')).to.equal('`,_');
77 | expect(tokens('SELECT ```', '`')).to.equal('`,_');
78 | expect(tokens('SELECT `\\`', '`')).to.equal('`,_');
79 | });
80 | it('replay error', () => {
81 | const lexer = mysqlLexer.makeLexer();
82 | expect(lexer('SELECT ')).to.equal(null);
83 | expect(() => lexer(' # ')).to.throw(
84 | Error, null, 'Expected delimiter at " # "');
85 | // Providing more input throws the same error.
86 | expect(() => lexer(' ')).to.throw(
87 | Error, null, 'Expected delimiter at " # "');
88 | });
89 | it('unfinished escape squence', () => {
90 | const lexer = mysqlLexer.makeLexer();
91 | expect(() => lexer('SELECT "\\')).to.throw(
92 | Error, null, 'Expected "\\\\" at "\\\\"');
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/test/mysql-tag-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | /* eslint no-magic-numbers: 0 */
19 |
20 | 'use strict';
21 |
22 | require('module-keys/cjs').polyfill(module, require);
23 |
24 | const { expect } = require('chai');
25 | const { describe, it } = require('mocha');
26 | const { mysql, SqlFragment, SqlId } = require('../index.js');
27 |
28 | const { Mintable } = require('node-sec-patterns');
29 |
30 | const isSqlFragment = Mintable.verifierFor(SqlFragment);
31 |
32 | function unwrapMinterFor(MintableType) {
33 | return require.moduleKeys.unbox(
34 | Mintable.minterFor(MintableType),
35 | () => true,
36 | () => {
37 | throw new Error('Cannot mint');
38 | });
39 | }
40 | const mintSqlFragment = unwrapMinterFor(SqlFragment);
41 | const mintSqlId = unwrapMinterFor(SqlId);
42 |
43 | function runTagTest(golden, test) {
44 | // Run multiply to test memoization bugs.
45 | for (let i = 3; --i >= 0;) {
46 | let result = test();
47 | if (result && isSqlFragment(result)) {
48 | result = result.content;
49 | } else {
50 | throw new Error(`Expected raw not ${ result }`);
51 | }
52 | expect(result).to.equal(golden);
53 | }
54 | }
55 |
56 | describe('mysql template tag', () => {
57 | it('numbers', () => {
58 | runTagTest(
59 | 'SELECT 2',
60 | () => mysql`SELECT ${ 1 + 1 }`);
61 | });
62 | it('date', () => {
63 | const date = new Date(Date.UTC(2000, 0, 1, 0, 0, 0));
64 | runTagTest(
65 | 'SELECT \'2000-01-01 00:00:00.000\'',
66 | () => mysql({ timeZone: 'GMT' })`SELECT ${ date }`);
67 | });
68 | it('string', () => {
69 | runTagTest(
70 | 'SELECT \'Hello, World!\\n\'',
71 | () => mysql`SELECT ${ 'Hello, World!\n' }`);
72 | });
73 | it('stringify', () => {
74 | const obj = {
75 | Hello: 'World!',
76 | toString() {
77 | return 'Hello, World!';
78 | },
79 | };
80 | runTagTest(
81 | 'SELECT \'Hello, World!\'',
82 | () => mysql({ stringifyObjects: true })`SELECT ${ obj }`);
83 | runTagTest(
84 | 'SELECT * FROM t WHERE `Hello` = \'World!\'',
85 | () => mysql({ stringifyObjects: false })`SELECT * FROM t WHERE ${ obj }`);
86 | });
87 | it('identifier', () => {
88 | runTagTest(
89 | 'SELECT `foo`',
90 | () => mysql`SELECT ${ mintSqlId('foo') }`);
91 | });
92 | it('blob', () => {
93 | runTagTest(
94 | 'SELECT "\x1f8p\xbe\\\'OlI\xb3\xe3\\Z\x0cg(\x95\x7f"',
95 | () =>
96 | mysql`SELECT "${ Buffer.from('1f3870be274f6c49b3e31a0c6728957f', 'hex') }"`
97 | );
98 | });
99 | it('null', () => {
100 | runTagTest(
101 | 'SELECT NULL',
102 | () =>
103 | mysql`SELECT ${ null }`
104 | );
105 | });
106 | it('undefined', () => {
107 | runTagTest(
108 | 'SELECT NULL',
109 | () =>
110 | mysql`SELECT ${ undefined }` // eslint-disable-line no-undefined
111 | );
112 | });
113 | it('negative zero', () => {
114 | runTagTest(
115 | 'SELECT (1 / 0)',
116 | () =>
117 | mysql`SELECT (1 / ${ -0 })`
118 | );
119 | });
120 | it('raw', () => {
121 | const raw = mintSqlFragment('1 + 1');
122 | runTagTest(
123 | 'SELECT 1 + 1',
124 | () => mysql`SELECT ${ raw }`);
125 | });
126 | it('string in dq string', () => {
127 | runTagTest(
128 | 'SELECT "Hello, World!\\n"',
129 | () => mysql`SELECT "Hello, ${ 'World!' }\n"`);
130 | });
131 | it('string in sq string', () => {
132 | runTagTest(
133 | 'SELECT \'Hello, World!\\n\'',
134 | () => mysql`SELECT 'Hello, ${ 'World!' }\n'`);
135 | });
136 | it('string after string in string', () => {
137 | // The following tests check obliquely that '?' is not
138 | // interpreted as a prepared statement meta-character
139 | // internally.
140 | runTagTest(
141 | 'SELECT \'Hello\', "World?"',
142 | () => mysql`SELECT '${ 'Hello' }', "World?"`);
143 | });
144 | it('string before string in string', () => {
145 | runTagTest(
146 | 'SELECT \'Hello?\', \'World?\'',
147 | () => mysql`SELECT 'Hello?', '${ 'World?' }'`);
148 | });
149 | it('number after string in string', () => {
150 | runTagTest(
151 | 'SELECT \'Hello?\', 123',
152 | () => mysql`SELECT '${ 'Hello?' }', ${ 123 }`);
153 | });
154 | it('number before string in string', () => {
155 | runTagTest(
156 | 'SELECT 123, \'World?\'',
157 | () => mysql`SELECT ${ 123 }, '${ 'World?' }'`);
158 | });
159 | it('string in identifier', () => {
160 | runTagTest(
161 | 'SELECT `foo`',
162 | () => mysql`SELECT \`${ 'foo' }\``);
163 | });
164 | it('identifier in identifier', () => {
165 | runTagTest(
166 | 'SELECT `foo`',
167 | () => mysql`SELECT \`${ mintSqlId('foo') }\``);
168 | });
169 | it('plain quoted identifier', () => {
170 | runTagTest(
171 | 'SELECT `ID`',
172 | () => mysql`SELECT \`ID\``);
173 | });
174 | it('backquotes in identifier', () => {
175 | runTagTest(
176 | 'SELECT `\\\\`',
177 | () => mysql`SELECT \`\\\``);
178 | const strings = [ 'SELECT `\\\\`' ];
179 | strings.raw = strings.slice();
180 | runTagTest('SELECT `\\\\`', () => mysql(strings));
181 | });
182 | it('backquotes in strings', () => {
183 | runTagTest(
184 | 'SELECT "`\\\\", \'`\\\\\'',
185 | () => mysql`SELECT "\`\\", '\`\\'`);
186 | });
187 | it('number in identifier', () => {
188 | runTagTest(
189 | 'SELECT `foo_123`',
190 | () => mysql`SELECT \`foo_${ 123 }\``);
191 | });
192 | it('array', () => {
193 | const id = mintSqlId('foo');
194 | const frag = mintSqlFragment('1 + 1');
195 | const values = [ 123, 'foo', id, frag ];
196 | runTagTest(
197 | 'SELECT X FROM T WHERE X IN (123, \'foo\', `foo`, 1 + 1)',
198 | () => mysql`SELECT X FROM T WHERE X IN (${ values })`);
199 | });
200 | it('unclosed-sq', () => {
201 | expect(() => mysql`SELECT '${ 'foo' }`).to.throw();
202 | });
203 | it('unclosed-dq', () => {
204 | expect(() => mysql`SELECT "foo`).to.throw();
205 | });
206 | it('unclosed-bq', () => {
207 | expect(() => mysql`SELECT \`${ 'foo' }`).to.throw();
208 | });
209 | it('unclosed-comment', () => {
210 | // Ending in a comment is a concatenation hazard.
211 | // See comments in lib/es6/Lexer.js.
212 | expect(() => mysql`SELECT (${ 0 }) -- comment`).to.throw();
213 | });
214 | it('merge-word-string', () => {
215 | runTagTest(
216 | 'SELECT utf8\'foo\'',
217 | () => mysql`SELECT utf8${ 'foo' }`);
218 | });
219 | it('merge-string-string', () => {
220 | runTagTest(
221 | // Adjacent string tokens are concatenated, but 'a''b' is a
222 | // 3-char string with a single-quote in the middle.
223 | 'SELECT \'a\' \'b\'',
224 | () => mysql`SELECT ${ 'a' }${ 'b' }`);
225 | });
226 | it('merge-bq-bq', () => {
227 | runTagTest(
228 | 'SELECT `a` `b`',
229 | () => mysql`SELECT ${ mintSqlId('a') }${ mintSqlId('b') }`);
230 | });
231 | it('merge-static-string-string', () => {
232 | runTagTest(
233 | 'SELECT \'a\' \'b\'',
234 | () => mysql`SELECT 'a'${ 'b' }`);
235 | });
236 | it('merge-string-static-string', () => {
237 | runTagTest(
238 | 'SELECT \'a\' \'b\'',
239 | () => mysql`SELECT ${ 'a' }'b'`);
240 | });
241 | it('not-a-merge-hazard', () => {
242 | runTagTest(
243 | 'SELECT \'a\'\'b\'',
244 | () => mysql`SELECT 'a''b'`);
245 | });
246 | });
247 |
--------------------------------------------------------------------------------
/test/pg-lexer-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | 'use strict';
19 |
20 | const { expect } = require('chai');
21 | const { describe, it } = require('mocha');
22 | const pgLexer = require('../lib/pg-lexer.js');
23 |
24 | function tokens(...chunks) {
25 | const { makeLexer } = pgLexer;
26 | const lexer = makeLexer();
27 | const out = [];
28 | for (let i = 0, len = chunks.length; i < len; ++i) {
29 | out.push(lexer(chunks[i]) || '_');
30 | }
31 | return out.join(',');
32 | }
33 |
34 | describe('pg template lexer', () => {
35 | it('empty string', () => {
36 | expect(tokens('')).to.equal('_');
37 | });
38 | it('hash comments', () => {
39 | // Unlike MySQL, postgres does not recognize # comments.
40 | expect(tokens(' # "foo\n', '')).to.equal('","');
41 | });
42 | it('dash comments', () => {
43 | expect(tokens(' -- \'foo\n', '')).to.equal('_,_');
44 | });
45 | it('dash dash in number literal', () => {
46 | // www.postgresql.org/docs/9.5/static/sql-syntax-lexical.html says
47 | // "-- and /* cannot appear anywhere in an operator name, since
48 | // they will be taken as the start of a comment."
49 | // so it looks like there is no rule similar to MySQL where "--"
50 | // when used as a comment delimiter has to not be immediately
51 | // preceded and followed by numeric or identifier characters.
52 | expect(() => tokens('SELECT (1--1)'))
53 | .to.throw(Error, 'Unterminated line comment: --1)');
54 | });
55 | it('block comments', () => {
56 | expect(tokens(' /* `foo */', '')).to.equal('_,_');
57 | expect(() => tokens(' /* `foo '))
58 | .to.throw(Error, 'Unterminated block comment: /* `foo');
59 | expect(tokens(' /* /* foo */ \' */', '')).to.equal('_,_');
60 | });
61 | it('dq', () => {
62 | expect(tokens('SELECT "foo"')).to.equal('_');
63 | expect(tokens('SELECT `foo`, "foo"')).to.equal('_');
64 | expect(tokens('SELECT "', '"')).to.equal('",_');
65 | expect(tokens('SELECT "x', '"')).to.equal('",_');
66 | expect(tokens('SELECT "\'', '"')).to.equal('",_');
67 | expect(tokens('SELECT "`', '"')).to.equal('",_');
68 | expect(tokens('SELECT """', '"')).to.equal('",_');
69 | // C-style escape sequences not supported in double
70 | // quoted strings unless U&
71 | expect(tokens('SELECT "\\"', '"')).to.equal('_,"');
72 | });
73 | it('U&dq', () => {
74 | expect(tokens('SELECT U&"foo"')).to.equal('_');
75 | expect(tokens('SELECT `foo`, U&"foo"')).to.equal('_');
76 | expect(tokens('SELECT U&"', '"')).to.equal('u&",_');
77 | expect(tokens('SELECT U&"x', '"')).to.equal('u&",_');
78 | expect(tokens('SELECT U&"\'', '"')).to.equal('u&",_');
79 | expect(tokens('SELECT U&"`', '"')).to.equal('u&",_');
80 | expect(tokens('SELECT U&"""', '"')).to.equal('u&",_');
81 | // C-style escape sequences not supported in double
82 | // quoted strings unless U&
83 | expect(tokens('SELECT U&"\\"', '"')).to.equal('u&",_');
84 | expect(() => tokens('SELECT U&"\\')).to.throw();
85 | });
86 | it('sq', () => {
87 | expect(tokens('SELECT \'foo\'')).to.equal('_');
88 | expect(tokens('SELECT `foo`, \'foo\'')).to.equal('_');
89 | expect(tokens('SELECT \'', '\'')).to.equal('\',_');
90 | expect(tokens('SELECT \'x', '\'')).to.equal('\',_');
91 | expect(tokens('SELECT \'"', '\'')).to.equal('\',_');
92 | expect(tokens('SELECT \'`', '\'')).to.equal('\',_');
93 | expect(tokens('SELECT \'\'\'', '\'')).to.equal('\',_');
94 | expect(tokens('SELECT \'\\\'', '\'')).to.equal('_,\'');
95 | });
96 | it('Esq', () => {
97 | expect(tokens('SELECT E\'foo\'')).to.equal('e');
98 | expect(tokens('SELECT E\'foo\';')).to.equal('_');
99 | expect(tokens('SELECT E\'foo')).to.equal('e\'');
100 | expect(tokens('SELECT `foo`, E\'foo\';')).to.equal('_');
101 | expect(tokens('SELECT E\'', '\';')).to.equal('e\',_');
102 | expect(tokens('SELECT E\'x', '\';')).to.equal('e\',_');
103 | expect(tokens('SELECT E\'"', '\';')).to.equal('e\',_');
104 | expect(tokens('SELECT E\'`', '\';')).to.equal('e\',_');
105 | expect(tokens('SELECT E\'\'\'', '\';')).to.equal('e\',_');
106 | expect(tokens('SELECT E\'\\\'', '\';')).to.equal('e\',_');
107 | expect(tokens('SELECT e\'\\\'', '\';')).to.equal('e\',_');
108 | // e' applies to subsequent single quoted strings.
109 | // 4.1.2.2. String Constants with C-style Escapes
110 | // (When continuing an escape string constant across lines,
111 | // write E only before the first opening quote.)
112 | expect(tokens('SELECT e\'foo\'\n \'\\\'')).to.equal('e\'');
113 | expect(tokens('SELECT e\'foo\' -- \n \'\\\'')).to.equal('e\'');
114 | expect(tokens('SELECT E\'foo\' /* */ \n \'\\\'')).to.equal('e\'');
115 | expect(tokens('SELECT e\'foo\' /* /**/\n*/ \'\\\'')).to.equal('e\'');
116 | // Check that we can look through interpolations for e'' continuations.
117 | expect(tokens('SELECT e\'foo', 'bar\' /* */ \n ', '\n ', ';')).to.equal('e\',e,e,_');
118 | });
119 | it('U&sq', () => {
120 | expect(tokens('SELECT U&\'foo\'')).to.equal('_');
121 | expect(tokens('SELECT `foo`, U&\'foo\'')).to.equal('_');
122 | expect(tokens('SELECT U&\'', '\'')).to.equal('u&\',_');
123 | expect(tokens('SELECT U&\'x', '\'')).to.equal('u&\',_');
124 | expect(tokens('SELECT U&\'"', '\'')).to.equal('u&\',_');
125 | expect(tokens('SELECT U&\'`', '\'')).to.equal('u&\',_');
126 | expect(tokens('SELECT U&\'\'\'', '\'')).to.equal('u&\',_');
127 | expect(tokens('SELECT U&\'\\\'', '\'')).to.equal('u&\',_');
128 | expect(tokens('SELECT u&\'\\\'', '\'')).to.equal('u&\',_');
129 | });
130 | it('$$', () => {
131 | expect(tokens('SELECT $$foo$$')).to.equal('_');
132 | expect(tokens('SELECT $$foo')).to.equal('$$');
133 | expect(tokens('SELECT $$foo', '$$')).to.equal('$$,_');
134 | expect(tokens('SELECT $$foo', 'bar$$')).to.equal('$$,_');
135 | expect(tokens('SELECT $$foo\\', 'bar$$')).to.equal('$$,_');
136 | expect(tokens('SELECT $foo$')).to.equal('$foo$');
137 | expect(tokens('SELECT $foo$bar')).to.equal('$foo$');
138 | expect(tokens('SELECT $foo$bar$foo$')).to.equal('_');
139 | expect(tokens('SELECT $$foo\\$$')).to.equal('_');
140 | expect(tokens('SELECT $foo$bar$baz$ ')).to.equal('$foo$');
141 |
142 | expect(() => tokens('SELECT $foo$ $'))
143 | .to.throw(Error, 'merge hazard \'$\' at end of $foo$ delimited string');
144 | expect(() => tokens('SELECT $foo$ $f'))
145 | .to.throw(Error, 'merge hazard \'$f\' at end of $foo$ delimited string');
146 | expect(() => tokens('SELECT $foo$ $fo'))
147 | .to.throw(Error, 'merge hazard \'$fo\' at end of $foo$ delimited string');
148 | expect(() => tokens('SELECT $foo$ $foo'))
149 | .to.throw(Error, 'merge hazard \'$foo\' at end of $foo$ delimited string');
150 | expect(() => tokens('SELECT $foo$ $x$'))
151 | .to.throw(Error, 'merge hazard \'$\' at end of $foo$ delimited string');
152 | expect(tokens('SELECT $foo$ $foo$')).to.equal('_');
153 | expect(tokens('SELECT $foo$ $x')).to.equal('$foo$');
154 | });
155 | it('replay error', () => {
156 | const lexer = pgLexer.makeLexer();
157 | expect(lexer('SELECT ')).to.equal(null);
158 | expect(() => lexer(' -- ')).to.throw(
159 | Error, 'Unterminated line comment: -- ');
160 | // Providing more input throws the same error.
161 | expect(() => lexer(' ')).to.throw(
162 | Error, 'Unterminated line comment: -- ');
163 | });
164 | it('unfinished escape squence', () => {
165 | const lexer = pgLexer.makeLexer();
166 | expect(() => lexer('SELECT E\'\\')).to.throw(
167 | Error, 'Incomplete escape sequence in e\' delimited string at `\\`');
168 | });
169 | });
170 |
--------------------------------------------------------------------------------
/test/pg-tag-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | /* eslint no-magic-numbers: 0 */
19 |
20 | 'use strict';
21 |
22 | require('module-keys/cjs').polyfill(module, require);
23 |
24 | const { expect } = require('chai');
25 | const { describe, it } = require('mocha');
26 | const { pg, SqlFragment, SqlId } = require('../index.js');
27 |
28 | const { Mintable } = require('node-sec-patterns');
29 |
30 | const isSqlFragment = Mintable.verifierFor(SqlFragment);
31 |
32 | function unwrapMinterFor(MintableType) {
33 | return require.moduleKeys.unbox(
34 | Mintable.minterFor(MintableType),
35 | () => true,
36 | () => {
37 | throw new Error('Cannot mint');
38 | });
39 | }
40 | const mintSqlFragment = unwrapMinterFor(SqlFragment);
41 | const mintSqlId = unwrapMinterFor(SqlId);
42 |
43 | function runTagTest(golden, test) {
44 | // Run multiply to test memoization bugs.
45 | for (let i = 3; --i >= 0;) {
46 | let result = test();
47 | if (result && isSqlFragment(result)) {
48 | result = result.content;
49 | } else {
50 | throw new Error(`Expected raw not ${ result }`);
51 | }
52 | expect(result).to.equal(golden);
53 | }
54 | }
55 |
56 | describe('pg template tag', () => {
57 | it('numbers', () => {
58 | runTagTest(
59 | 'SELECT 2',
60 | () => pg`SELECT ${ 1 + 1 }`);
61 | });
62 | it('date', () => {
63 | const date = new Date(Date.UTC(2000, 0, 1, 0, 0, 0));
64 | runTagTest(
65 | 'SELECT \'2000-01-01 00:00:00.000\'',
66 | () => pg({ timeZone: 'GMT' })`SELECT ${ date }`);
67 | });
68 | it('string', () => {
69 | runTagTest(
70 | 'SELECT e\'Hello, World!\\n\'',
71 | () => pg`SELECT ${ 'Hello, World!\n' }`);
72 | });
73 | it('stringify', () => {
74 | const obj = {
75 | Hello: 'World!',
76 | toString() {
77 | return 'Hello, World!';
78 | },
79 | };
80 | runTagTest(
81 | 'SELECT \'Hello, World!\'',
82 | () => pg({ stringifyObjects: true })`SELECT ${ obj }`);
83 | runTagTest(
84 | 'SELECT * FROM t WHERE "Hello" = \'World!\'',
85 | () => pg({ stringifyObjects: false })`SELECT * FROM t WHERE ${ obj }`);
86 | });
87 | describe('identifier', () => {
88 | const str = 'O\'Reilly the "Unescaped"';
89 | const id = mintSqlId(str);
90 | it('bare id', () => {
91 | runTagTest('SELECT "O\'Reilly the ""Unescaped"""', () => pg`SELECT ${ id }`);
92 | });
93 | it('dq str', () => {
94 | runTagTest('SELECT "O\'Reilly the ""Unescaped"""', () => pg`SELECT "${ str }"`);
95 | });
96 | it('dq id', () => {
97 | runTagTest('SELECT "O\'Reilly the ""Unescaped"""', () => pg`SELECT "${ id }"`);
98 | });
99 | it('U&dq str', () => {
100 | runTagTest('SELECT U&"O\\0027Reilly the \\0022Unescaped\\0022"', () => pg`SELECT U&"${ str }"`);
101 | });
102 | it('U&dq id', () => {
103 | runTagTest('SELECT U&"O\\0027Reilly the \\0022Unescaped\\0022"', () => pg`SELECT U&"${ id }"`);
104 | });
105 | });
106 | describe('blob', () => {
107 | const blob = Buffer.from('1f3870be274f6c49b3e31a0c6728957f', 'hex');
108 | it('x', () => {
109 | runTagTest(
110 | 'SELECT x\'1f3870be274f6c49b3e31a0c6728957f\'',
111 | () => pg`SELECT x'${ blob }'`
112 | );
113 | });
114 | it('b', () => {
115 | runTagTest(
116 | 'SELECT b\'000111110011100001110000101111100010011101001111011011000100' +
117 | '10011011001111100011000110100000110001100111001010001001010101111111\'',
118 | () => pg`SELECT b'${ blob }'`
119 | );
120 | });
121 | it('e', () => {
122 | runTagTest(
123 | 'SELECT e\'\x1f8p\xbe\'\'OlI\xb3\xe3\\x1a\x0cg(\x95\x7f\'',
124 | () => pg`SELECT e'${ blob }'`
125 | );
126 | });
127 | it('u&', () => {
128 | runTagTest(
129 | 'SELECT u&\'\x1f8p\xbe\\0027OlI\xb3\xe3\\001a\x0cg(\x95\x7f\'',
130 | () => pg`SELECT u&'${ blob }'`
131 | );
132 | });
133 | it('raw', () => {
134 | runTagTest(
135 | 'SELECT \'\x1f8p\xbe\'\'OlI\xb3\xe3\x1a\x0cg(\x95\x7f\'',
136 | () => pg`SELECT '${ blob }'`
137 | );
138 | });
139 | it('$$', () => {
140 | runTagTest(
141 | 'SELECT $$\x1f8p\xbe\'OlI\xb3\xe3\x1a\x0cg(\x95\x7f$$',
142 | () => pg`SELECT $$${ blob }$$`
143 | );
144 | });
145 | });
146 | it('null', () => {
147 | runTagTest(
148 | 'SELECT NULL',
149 | () =>
150 | pg`SELECT ${ null }`
151 | );
152 | });
153 | it('undefined', () => {
154 | runTagTest(
155 | 'SELECT NULL',
156 | () =>
157 | pg`SELECT ${ undefined }` // eslint-disable-line no-undefined
158 | );
159 | });
160 | it('negative zero', () => {
161 | runTagTest(
162 | 'SELECT (1 / 0)',
163 | () =>
164 | pg`SELECT (1 / ${ -0 })`
165 | );
166 | });
167 | it('raw', () => {
168 | const raw = mintSqlFragment('1 + 1');
169 | runTagTest(
170 | 'SELECT 1 + 1',
171 | () => pg`SELECT ${ raw }`);
172 | });
173 | it('string in dq string', () => {
174 | runTagTest(
175 | 'SELECT "Hello, World!\\n"',
176 | () => pg`SELECT "Hello, ${ 'World!' }\n"`);
177 | });
178 | it('string in sq string', () => {
179 | runTagTest(
180 | 'SELECT \'Hello, World!\\n\'',
181 | () => pg`SELECT 'Hello, ${ 'World!' }\n'`);
182 | });
183 | it('string after string in string', () => {
184 | // The following tests check obliquely that '?' is not
185 | // interpreted as a prepared statement meta-character
186 | // internally.
187 | runTagTest(
188 | 'SELECT \'Hello\', "World?"',
189 | () => pg`SELECT '${ 'Hello' }', "World?"`);
190 | });
191 | it('string before string in string', () => {
192 | runTagTest(
193 | 'SELECT \'Hello?\', \'World?\'',
194 | () => pg`SELECT 'Hello?', '${ 'World?' }'`);
195 | });
196 | it('number after string in string', () => {
197 | runTagTest(
198 | 'SELECT \'Hello?\', 123',
199 | () => pg`SELECT '${ 'Hello?' }', ${ 123 }`);
200 | });
201 | it('number before string in string', () => {
202 | runTagTest(
203 | 'SELECT 123, \'World?\'',
204 | () => pg`SELECT ${ 123 }, '${ 'World?' }'`);
205 | });
206 | it('string in identifier', () => {
207 | runTagTest(
208 | 'SELECT "foo"',
209 | () => pg`SELECT "${ 'foo' }"`);
210 | });
211 | it('identifier in identifier', () => {
212 | runTagTest(
213 | 'SELECT "foo"',
214 | () => pg`SELECT "${ mintSqlId('foo') }"`);
215 | });
216 | it('plain quoted identifier', () => {
217 | runTagTest(
218 | 'SELECT "ID"',
219 | () => pg`SELECT "ID"`);
220 | });
221 | it('dqs in identifier', () => {
222 | runTagTest(
223 | 'SELECT "\\\\"',
224 | () => pg`SELECT "\\"`);
225 | const strings = [ 'SELECT "\\\\"' ];
226 | strings.raw = strings.slice();
227 | runTagTest('SELECT "\\\\"', () => pg(strings));
228 | });
229 | it('backquotes in strings', () => {
230 | runTagTest(
231 | 'SELECT "\\`\\\\", \'\\`\\\\\'',
232 | () => pg`SELECT "\`\\", '\`\\'`);
233 | });
234 | it('number in identifier', () => {
235 | runTagTest(
236 | 'SELECT "foo_123"',
237 | () => pg`SELECT "foo_${ 123 }"`);
238 | });
239 | it('array', () => {
240 | const id = mintSqlId('foo');
241 | const frag = mintSqlFragment('1 + 1');
242 | const values = [ 123, 'foo', id, frag ];
243 | runTagTest(
244 | 'SELECT X FROM T WHERE X IN (123, \'foo\', "foo", 1 + 1)',
245 | () => pg`SELECT X FROM T WHERE X IN (${ values })`);
246 | });
247 | it('unclosed-sq', () => {
248 | expect(() => pg`SELECT '${ 'foo' }`).to.throw();
249 | });
250 | it('unclosed-dq', () => {
251 | expect(() => pg`SELECT "foo`).to.throw();
252 | });
253 | it('unclosed-dq-interp', () => {
254 | expect(() => pg`SELECT "${ 'foo' }`).to.throw();
255 | });
256 | it('unclosed-comment', () => {
257 | // Ending in a comment is a concatenation hazard.
258 | // See comments in lib/es6/Lexer.js.
259 | expect(() => pg`SELECT (${ 0 }) -- comment`).to.throw();
260 | });
261 | it('merge-word-string', () => {
262 | runTagTest(
263 | 'SELECT utf8\'foo\'',
264 | () => pg`SELECT utf8${ 'foo' }`);
265 | });
266 | it('merge-string-string', () => {
267 | runTagTest(
268 | // Adjacent string tokens are concatenated, but 'a''b' is a
269 | // 3-char string with a single-quote in the middle.
270 | 'SELECT \'a\' \'b\'',
271 | () => pg`SELECT ${ 'a' }${ 'b' }`);
272 | });
273 | it('merge-id-id', () => {
274 | runTagTest(
275 | 'SELECT "a" "b"',
276 | () => pg`SELECT ${ mintSqlId('a') }${ mintSqlId('b') }`);
277 | });
278 | it('merge-static-string-string', () => {
279 | runTagTest(
280 | 'SELECT \'a\' \'b\'',
281 | () => pg`SELECT 'a'${ 'b' }`);
282 | });
283 | it('merge-string-static-string', () => {
284 | runTagTest(
285 | 'SELECT \'a\' \'b\'',
286 | () => pg`SELECT ${ 'a' }'b'`);
287 | });
288 | it('not-a-merge-hazard', () => {
289 | runTagTest(
290 | 'SELECT \'a\'\'b\'',
291 | () => pg`SELECT 'a''b'`);
292 | });
293 | describe('literal-string-corner-cases', () => {
294 | it('$$', () => {
295 | runTagTest(
296 | 'SELECT $$x$$',
297 | () => pg`SELECT $$${ 'x' }$$`);
298 | });
299 | it('$$ hazard', () => {
300 | expect(() => pg`SELECT $$${ '$$' }$$`).to.throw(Error, 'Cannot embed ');
301 | expect(() => pg`SELECT $$${ 'x$$x' }$$`).to.throw(Error, 'Cannot embed ');
302 | expect(() => pg`SELECT $$${ 'x$' }$$`).to.throw(Error, 'Cannot embed ');
303 | });
304 | it('$foo$', () => {
305 | runTagTest(
306 | 'SELECT $foo$x$foo$',
307 | () => pg`SELECT $foo$${ 'x' }$foo$`);
308 | runTagTest(
309 | 'SELECT $foo$x$foox$foo$',
310 | () => pg`SELECT $foo$${ 'x$foox' }$foo$`);
311 | runTagTest(
312 | 'SELECT $foo$x$bar$x$foo$',
313 | () => pg`SELECT $foo$${ 'x$bar$x' }$foo$`);
314 | runTagTest(
315 | 'SELECT $foo$$bar$x$foo$',
316 | () => pg`SELECT $foo$${ '$bar$x' }$foo$`);
317 | runTagTest(
318 | 'SELECT $foo$$$x$foo$',
319 | () => pg`SELECT $foo$${ '$$x' }$foo$`);
320 | });
321 | it('$foo$ hazard', () => {
322 | expect(() => pg`SELECT $foo$${ '$foo$' }$foo$`).to.throw(Error, 'Cannot embed ');
323 | expect(() => pg`SELECT $foo$${ 'x$foo$x' }$foo$`).to.throw(Error, 'Cannot embed ');
324 | expect(() => pg`SELECT $foo$${ 'x$fo' }$foo$`).to.throw(Error, 'Cannot embed ');
325 |
326 | expect(() => pg`SELECT $foo$${ '$fOo$x' }$foo$`).to.not.throw();
327 | });
328 | it('mixed case hazard', () => {
329 | // OK
330 | runTagTest(
331 | 'SELECT $foo$ $foo$, e\'\\$foo\\$\', "$foo$--"\n',
332 | () => pg`SELECT $foo$ $foo$, ${ '$foo$' }, "$foo$--"
333 | `);
334 | // Mixed case matters
335 | expect(() => pg`SELECT $foo$ $Foo$, ${ '$foo$' } "$foo$--"
336 | `)
337 | .to.throw(Error, 'Cannot embed ');
338 | });
339 | });
340 | describe('continued-strings', () => {
341 | const line = [ 'haven\'t', 'have too', 'have not! n\'t!' ];
342 | it('e', () => {
343 | runTagTest(
344 | 'SELECT e\'\' \'haven\'\'t\'\n \'have too\'\n \'have not! n\'\'t!\'',
345 | () =>
346 | pg`SELECT e'' ${ line[0] }
347 | ${ line[1] }
348 | ${ line[2] }`);
349 | });
350 | it('ambiguity', () => {
351 | expect(
352 | () =>
353 | pg`SELECT ${ line[0] }
354 | ${ line[1] }
355 | ${ line[2] }`)
356 | .to.throw(Error, 'Potential for ambiguous string continuation');
357 | });
358 | });
359 | describe('meta-chars', () => {
360 | const cases = [
361 | {
362 | metachar: '\'',
363 | want: String.raw`SELECT '''' AS "'", u&'${ '\\' }0027' AS u&"${ '\\' }0027", e''''`,
364 | },
365 | {
366 | metachar: '"',
367 | want: String.raw`SELECT '"' AS """", u&'${ '\\' }0022' AS u&"${ '\\' }0022", e'\"'`,
368 | },
369 | {
370 | metachar: '\0',
371 | want: String.raw`SELECT '' AS "", u&'' AS u&"", e''`,
372 | },
373 | {
374 | metachar: '\\',
375 | want: String.raw`SELECT '\' AS "\", u&'${ '\\' }005c' AS u&"${ '\\' }005c", e'\\'`,
376 | },
377 | {
378 | metachar: '\n',
379 | want: String.raw`SELECT '${ '\n' }' AS "${ '\n' }", u&'${ '\\' }000a' AS u&"${ '\\' }000a", e'\n'`,
380 | },
381 | {
382 | metachar: '',
383 | want: String.raw`SELECT '' AS "", u&'' AS u&"", e''`,
384 | },
385 | ];
386 | for (const { metachar, want } of cases) {
387 | it(`Escaping of ${ JSON.stringify(metachar) }`, () => {
388 | const got =
389 | pg`SELECT '${ metachar }' AS "${ metachar }", u&'${ metachar }' AS u&"${ metachar }", e'${ metachar }'`;
390 | expect(got.content).to.equal(want, metachar);
391 | // TODO: maybe try to actually issue queries and check the results.
392 | // 15 Nov 2019 - manually checked the wanted SQL against psql (PostgreSQL) 10.5
393 | // conencted to a server with the default configuration produced by initdb.
394 | });
395 | }
396 | });
397 | });
398 |
--------------------------------------------------------------------------------