mssql 9.2.0 → 9.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Microsoft SQL Server client for Node.js
4
4
 
5
- [![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [![Appveyor CI][appveyor-image]][appveyor-url] [![Join the chat at https://gitter.im/patriksimek/node-mssql](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/patriksimek/node-mssql?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
5
+ [![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [![Join the chat at https://gitter.im/patriksimek/node-mssql](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/patriksimek/node-mssql?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
6
6
 
7
7
  Supported TDS drivers:
8
8
  - [Tedious][tedious-url] (pure JavaScript - Windows/macOS/Linux, default)
@@ -108,6 +108,13 @@ async () => {
108
108
  * [bulk](#bulk-table-options-callback)
109
109
  * [cancel](#cancel)
110
110
 
111
+ ### Identifier rules
112
+
113
+ * [Parameter names](#parameter-names)
114
+ * [Column names](#column-names)
115
+ * [Type names and sizes](#type-names-and-sizes)
116
+ * [Procedure names](#procedure-names)
117
+
111
118
  ### Transactions
112
119
 
113
120
  * [Transaction](#transaction)
@@ -746,6 +753,7 @@ request.execute('procedure_name', (err, result) => {
746
753
 
747
754
  __Errors__
748
755
  - EREQUEST (`RequestError`) - *Message from SQL Server*
756
+ - EINJECT (`RequestError`) - Procedure name or parameter type is not a valid identifier. See [Identifiers](#identifiers).
749
757
  - ECANCEL (`RequestError`) - Cancelled.
750
758
  - ETIMEOUT (`RequestError`) - Request timeout.
751
759
  - ENOCONN (`RequestError`) - No connection is specified for that request.
@@ -798,7 +806,7 @@ sql.map.register(Number, sql.BigInt)
798
806
 
799
807
  __Errors__ (synchronous)
800
808
  - EARGS (`RequestError`) - Invalid number of arguments.
801
- - EINJECT (`RequestError`) - SQL injection warning.
809
+ - EINJECT (`RequestError`) - Parameter name is not a valid identifier. See [Identifiers](#identifiers).
802
810
 
803
811
  ---------------------------------------
804
812
 
@@ -823,7 +831,7 @@ request.output('output_parameter', sql.VarChar(50), 'abc')
823
831
 
824
832
  __Errors__ (synchronous)
825
833
  - EARGS (`RequestError`) - Invalid number of arguments.
826
- - EINJECT (`RequestError`) - SQL injection warning.
834
+ - EINJECT (`RequestError`) - Parameter name is not a valid identifier. See [Identifiers](#identifiers).
827
835
 
828
836
  ---------------------------------------
829
837
 
@@ -907,6 +915,7 @@ __Errors__
907
915
  - ECONNCLOSED (`ConnectionError`) - Connection is closed.
908
916
  - ENOTBEGUN (`TransactionError`) - Transaction has not begun.
909
917
  - EABORT (`TransactionError`) - Transaction was aborted (by user or because of an error).
918
+ - EINJECT (`RequestError`) - Parameter type is not a valid identifier. See [Identifiers](#identifiers).
910
919
 
911
920
  ```javascript
912
921
  const request = new sql.Request()
@@ -952,6 +961,7 @@ __Errors__
952
961
  - ECONNCLOSED (`ConnectionError`) - Connection is closed.
953
962
  - ENOTBEGUN (`TransactionError`) - Transaction has not begun.
954
963
  - EABORT (`TransactionError`) - Transaction was aborted (by user or because of an error).
964
+ - EINJECT (`RequestError`) - Parameter type is not a valid identifier. See [Identifiers](#identifiers).
955
965
 
956
966
  You can enable multiple recordsets in queries with the `request.multiple = true` command.
957
967
 
@@ -998,6 +1008,7 @@ __Errors__
998
1008
  - ECONNCLOSED (`ConnectionError`) - Connection is closed.
999
1009
  - ENOTBEGUN (`TransactionError`) - Transaction has not begun.
1000
1010
  - EABORT (`TransactionError`) - Transaction was aborted (by user or because of an error).
1011
+ - EINJECT (`RequestError`) - Column name or parameter type is not a valid identifier. See [Identifiers](#identifiers).
1001
1012
 
1002
1013
  ---------------------------------------
1003
1014
 
@@ -1228,7 +1239,7 @@ ps.input('input_parameter', sql.VarChar(50))
1228
1239
 
1229
1240
  __Errors__ (synchronous)
1230
1241
  - EARGS (`PreparedStatementError`) - Invalid number of arguments.
1231
- - EINJECT (`PreparedStatementError`) - SQL injection warning.
1242
+ - EINJECT (`PreparedStatementError`) - Parameter name is not a valid identifier. See [Identifiers](#identifiers).
1232
1243
 
1233
1244
  ---------------------------------------
1234
1245
 
@@ -1250,7 +1261,7 @@ ps.output('output_parameter', sql.VarChar(50))
1250
1261
 
1251
1262
  __Errors__ (synchronous)
1252
1263
  - EARGS (`PreparedStatementError`) - Invalid number of arguments.
1253
- - EINJECT (`PreparedStatementError`) - SQL injection warning.
1264
+ - EINJECT (`PreparedStatementError`) - Parameter name is not a valid identifier. See [Identifiers](#identifiers).
1254
1265
 
1255
1266
  ---------------------------------------
1256
1267
 
@@ -1823,6 +1834,100 @@ Results in:
1823
1834
  ]
1824
1835
  ```
1825
1836
 
1837
+ ## Identifiers
1838
+
1839
+ Values are sent to the server as parameters and may hold anything. A few things
1840
+ are not values, and cannot be: the name of a parameter, the name of a column in a
1841
+ bulk load, the type a parameter is declared as, and the name of a stored procedure.
1842
+ Those are *identifiers*, and
1843
+ no SQL database can bind them, so this library builds them into the statement it
1844
+ sends. It checks them, and rejects anything that could end the identifier and have
1845
+ the rest of the value read as SQL.
1846
+
1847
+ That check is not a substitute for your own. If any of these comes from somewhere
1848
+ you do not control, map it to a value you expect first:
1849
+
1850
+ ```javascript
1851
+ const columns = { name: 'name', created: 'created_at' } // what you allow
1852
+ const column = columns[req.query.sortBy] // what they asked for
1853
+ if (!column) throw new Error('unknown column')
1854
+
1855
+ request.input(column, sql.NVarChar, value)
1856
+ ```
1857
+
1858
+ ### Parameter names
1859
+
1860
+ `request.input()`, `request.output()` and the same methods on a prepared statement
1861
+ take a name without the leading `@`. It may hold letters, digits, marks and
1862
+ connector punctuation from any script, along with `@`, `#`, `$` and the two
1863
+ zero-width joiners. A name that is not a string or a number is rejected, as is one
1864
+ holding an invisible format character, because one of them is a separator the
1865
+ server treats as ending the identifier.
1866
+
1867
+ Anything else raises `EINJECT`. Note that a name this library accepts may still be
1868
+ one the server rejects; the check is deliberately wider than the server's own rules
1869
+ so that it only refuses what could change the shape of the statement.
1870
+
1871
+ ### Column names
1872
+
1873
+ `table.columns.add()` names a column in a bulk load, and the name is emitted as a
1874
+ quoted identifier. A `]` ends that quoting, so a name holding one must double it,
1875
+ which is how T-SQL escapes it:
1876
+
1877
+ ```javascript
1878
+ table.columns.add('a]]b', sql.Int) // the column named a]b
1879
+ ```
1880
+
1881
+ A single `]` raises `EINJECT`. Column names are not otherwise restricted: spaces,
1882
+ punctuation and reserved words are all fine, because the quoting handles them.
1883
+
1884
+ The keys of `options.order` are also identifiers, but the driver writes them into the bulk
1885
+ statement's `ORDER (...)` clause *unquoted* rather than as `[name]`. They follow the type
1886
+ name rule instead: bare, or quoted with brackets or double quotes.
1887
+
1888
+ ```javascript
1889
+ request.bulk(table, { order: { created_at: 'ASC' } })
1890
+ request.bulk(table, { order: { '[Created At]': 'DESC' } }) // quote a spaced name yourself
1891
+ ```
1892
+
1893
+ ### Type names and sizes
1894
+
1895
+ `sql.TVP()` takes the name of a table type, optionally qualified with a schema and
1896
+ optionally quoted with brackets or double quotes. A size — the length, precision or
1897
+ scale of a type such as `sql.VarChar()` — must be a number, or the word `max` the
1898
+ declaration accepts in its place. Padding is trimmed.
1899
+
1900
+ ```javascript
1901
+ request.input('rows', sql.TVP('dbo.[My Type]'), table)
1902
+ request.input('name', sql.VarChar(50), value)
1903
+ request.input('note', sql.VarChar('max'), value)
1904
+ ```
1905
+
1906
+ A size is checked wherever it is built into SQL, which includes the declaration a
1907
+ bulk load sends and the parameter list a query sends, not only `sql.TVP()`.
1908
+
1909
+ ### Procedure names
1910
+
1911
+ `request.execute()` takes the name of a stored procedure, optionally qualified with
1912
+ a server, database and schema and optionally quoted with brackets or double quotes.
1913
+ An omitted part may be left empty, as T-SQL allows. The `msnodesqlv8` driver builds
1914
+ this into the statement it sends; the `tedious` driver sends it as a bound remote
1915
+ procedure call and could not be injected through it. Both check it, so a name is
1916
+ accepted or rejected the same way whichever driver you use.
1917
+
1918
+ ```javascript
1919
+ request.execute('dbo.[My Procedure]')
1920
+ request.execute('master..sp_who') // an omitted schema is fine
1921
+ ```
1922
+
1923
+ The first and last parts must be present, so `.proc` and `db..` are rejected.
1924
+
1925
+ Anything else raises `EINJECT`. Numbered procedures — the deprecated
1926
+ `procedure;1` form — are rejected, because the `;` cannot be told apart from the
1927
+ end of a statement.
1928
+
1929
+ ---------------------------------------
1930
+
1826
1931
  ## Errors
1827
1932
 
1828
1933
  There are 4 types of errors you can handle:
@@ -1859,10 +1964,10 @@ Name | Code | Message
1859
1964
  `RequestError` | ECANCEL | Cancelled.
1860
1965
  `RequestError` | ETIMEOUT | Request timeout.
1861
1966
  `RequestError` | EARGS | Invalid number of arguments.
1862
- `RequestError` | EINJECT | SQL injection warning.
1967
+ `RequestError` | EINJECT | Parameter name, column name or type is not a valid identifier.
1863
1968
  `RequestError` | ENOCONN | No connection is specified for that request.
1864
1969
  `PreparedStatementError` | EARGS | Invalid number of arguments.
1865
- `PreparedStatementError` | EINJECT | SQL injection warning.
1970
+ `PreparedStatementError` | EINJECT | Parameter name is not a valid identifier.
1866
1971
  `PreparedStatementError` | EALREADYPREPARED | Statement is already prepared.
1867
1972
  `PreparedStatementError` | ENOTPREPARED | Statement is not prepared.
1868
1973
 
@@ -2093,8 +2198,6 @@ to create new connections or not
2093
2198
  [downloads-url]: https://www.npmjs.com/package/mssql
2094
2199
  [david-image]: https://img.shields.io/david/tediousjs/node-mssql.svg?style=flat-square
2095
2200
  [david-url]: https://david-dm.org/tediousjs/node-mssql
2096
- [appveyor-image]: https://ci.appveyor.com/api/projects/status/e5gq1a0ujwams9t7/branch/master?svg=true
2097
- [appveyor-url]: https://ci.appveyor.com/project/tediousjs/node-mssql
2098
2201
 
2099
2202
  [tedious-url]: https://www.npmjs.com/package/tedious
2100
2203
  [msnodesqlv8-url]: https://www.npmjs.com/package/msnodesqlv8
@@ -2,7 +2,7 @@
2
2
 
3
3
  const debug = require('debug')('mssql:base')
4
4
  const { EventEmitter } = require('events')
5
- const { IDS, objectHasProperty } = require('../utils')
5
+ const { IDS, objectHasProperty, isValidParameterName } = require('../utils')
6
6
  const globalConnection = require('../global-connection')
7
7
  const { TransactionError, PreparedStatementError } = require('../error')
8
8
  const shared = require('../shared')
@@ -91,8 +91,8 @@ class PreparedStatement extends EventEmitter {
91
91
  */
92
92
 
93
93
  input (name, type) {
94
- if ((/(--| |\/\*|\*\/|')/).test(name)) {
95
- throw new PreparedStatementError(`SQL injection warning for param '${name}'`, 'EINJECT')
94
+ if (!isValidParameterName(name)) {
95
+ throw new PreparedStatementError(`Invalid parameter name '${name}'. Parameter names may only contain identifier characters`, 'EINJECT')
96
96
  }
97
97
 
98
98
  if (arguments.length < 2) {
@@ -144,8 +144,8 @@ class PreparedStatement extends EventEmitter {
144
144
  */
145
145
 
146
146
  output (name, type) {
147
- if (/(--| |\/\*|\*\/|')/.test(name)) {
148
- throw new PreparedStatementError(`SQL injection warning for param '${name}'`, 'EINJECT')
147
+ if (!isValidParameterName(name)) {
148
+ throw new PreparedStatementError(`Invalid parameter name '${name}'. Parameter names may only contain identifier characters`, 'EINJECT')
149
149
  }
150
150
 
151
151
  if (arguments.length < 2) {
@@ -235,17 +235,25 @@ class PreparedStatement extends EventEmitter {
235
235
  const req = new shared.driver.Request(this)
236
236
  req.stream = false
237
237
  req.output('handle', TYPES.Int)
238
- req.input('params', TYPES.NVarChar, ((() => {
239
- const result = []
238
+ let params
239
+ try {
240
+ params = []
240
241
  for (const name in this.parameters) {
241
242
  if (!objectHasProperty(this.parameters, name)) {
242
243
  continue
243
244
  }
244
245
  const param = this.parameters[name]
245
- result.push(`@${name} ${declare(param.type, param)}${param.io === 2 ? ' output' : ''}`)
246
+ // a connection is held here, so a rejected declaration has to be reported
247
+ // through the path that releases it
248
+ params.push(`@${name} ${declare(param.type, param)}${param.io === 2 ? ' output' : ''}`)
246
249
  }
247
- return result
248
- })()).join(','))
250
+ } catch (e) {
251
+ this.parent.release(this._acquiredConnection)
252
+ this._acquiredConnection = null
253
+ this._acquiredConfig = null
254
+ return callback(new PreparedStatementError(e, 'EREQUEST'))
255
+ }
256
+ req.input('params', TYPES.NVarChar, params.join(','))
249
257
  req.input('stmt', TYPES.NVarChar, this.statement)
250
258
  req.execute('sp_prepare', (err, result) => {
251
259
  if (err) {
@@ -3,7 +3,7 @@
3
3
  const debug = require('debug')('mssql:base')
4
4
  const { EventEmitter } = require('events')
5
5
  const { Readable } = require('stream')
6
- const { IDS, objectHasProperty } = require('../utils')
6
+ const { IDS, objectHasProperty, isValidParameterName } = require('../utils')
7
7
  const globalConnection = require('../global-connection')
8
8
  const { RequestError, ConnectionError } = require('../error')
9
9
  const { TYPES } = require('../datatypes')
@@ -107,8 +107,8 @@ class Request extends EventEmitter {
107
107
  */
108
108
 
109
109
  input (name, type, value) {
110
- if ((/(--| |\/\*|\*\/|')/).test(name)) {
111
- throw new RequestError(`SQL injection warning for param '${name}'`, 'EINJECT')
110
+ if (!isValidParameterName(name)) {
111
+ throw new RequestError(`Invalid parameter name '${name}'. Parameter names may only contain identifier characters`, 'EINJECT')
112
112
  }
113
113
 
114
114
  if (arguments.length < 2) {
@@ -170,8 +170,8 @@ class Request extends EventEmitter {
170
170
  output (name, type, value) {
171
171
  if (!type) { type = TYPES.NVarChar }
172
172
 
173
- if ((/(--| |\/\*|\*\/|')/).test(name)) {
174
- throw new RequestError(`SQL injection warning for param '${name}'`, 'EINJECT')
173
+ if (!isValidParameterName(name)) {
174
+ throw new RequestError(`Invalid parameter name '${name}'. Parameter names may only contain identifier characters`, 'EINJECT')
175
175
  }
176
176
 
177
177
  if ((type === TYPES.Text) || (type === TYPES.NText) || (type === TYPES.Image)) {
package/lib/datatypes.js CHANGED
@@ -1,5 +1,5 @@
1
1
  'use strict'
2
- const objectHasProperty = require('./utils').objectHasProperty
2
+ const { objectHasProperty, assertSafeTypeName, assertSafeTypeSize } = require('./utils')
3
3
  const inspect = Symbol.for('nodejs.util.inspect.custom')
4
4
 
5
5
  const TYPES = {
@@ -132,19 +132,33 @@ for (const key in TYPES) {
132
132
  }
133
133
 
134
134
  module.exports.declare = (type, options) => {
135
+ // sizes and the type name reach the generated declaration directly, so they are checked
136
+ // here rather than trusted from whatever built the parameter. Each is read only in the
137
+ // branch that emits it, so a type that takes no size still needs no options.
135
138
  switch (type) {
136
- case TYPES.VarChar: case TYPES.VarBinary:
137
- return `${type.declaration} (${options.length > 8000 ? 'MAX' : (options.length == null ? 'MAX' : options.length)})`
138
- case TYPES.NVarChar:
139
- return `${type.declaration} (${options.length > 4000 ? 'MAX' : (options.length == null ? 'MAX' : options.length)})`
140
- case TYPES.Char: case TYPES.NChar: case TYPES.Binary:
141
- return `${type.declaration} (${options.length == null ? 1 : options.length})`
142
- case TYPES.Decimal: case TYPES.Numeric:
143
- return `${type.declaration} (${options.precision == null ? 18 : options.precision}, ${options.scale == null ? 0 : options.scale})`
144
- case TYPES.Time: case TYPES.DateTime2: case TYPES.DateTimeOffset:
145
- return `${type.declaration} (${options.scale == null ? 7 : options.scale})`
139
+ case TYPES.VarChar: case TYPES.VarBinary: {
140
+ const length = assertSafeTypeSize(options.length)
141
+ return `${type.declaration} (${length > 8000 ? 'MAX' : (length == null ? 'MAX' : length)})`
142
+ }
143
+ case TYPES.NVarChar: {
144
+ const length = assertSafeTypeSize(options.length)
145
+ return `${type.declaration} (${length > 4000 ? 'MAX' : (length == null ? 'MAX' : length)})`
146
+ }
147
+ case TYPES.Char: case TYPES.NChar: case TYPES.Binary: {
148
+ const length = assertSafeTypeSize(options.length)
149
+ return `${type.declaration} (${length == null ? 1 : length})`
150
+ }
151
+ case TYPES.Decimal: case TYPES.Numeric: {
152
+ const precision = assertSafeTypeSize(options.precision)
153
+ const scale = assertSafeTypeSize(options.scale)
154
+ return `${type.declaration} (${precision == null ? 18 : precision}, ${scale == null ? 0 : scale})`
155
+ }
156
+ case TYPES.Time: case TYPES.DateTime2: case TYPES.DateTimeOffset: {
157
+ const scale = assertSafeTypeSize(options.scale)
158
+ return `${type.declaration} (${scale == null ? 7 : scale})`
159
+ }
146
160
  case TYPES.TVP:
147
- return `${options.tvpType} readonly`
161
+ return `${assertSafeTypeName(options.tvpType)} readonly`
148
162
  default:
149
163
  return type.declaration
150
164
  }
@@ -24,7 +24,7 @@ class ConnectionPool extends BaseConnectionPool {
24
24
  if (!this.config.connectionString) {
25
25
  cfg.conn_str = buildConnectionString({
26
26
  Driver: CONNECTION_DRIVER,
27
- Server: this.config.options.instanceName ? `${this.config.server}\\${this.config.instanceName}` : `${this.config.server},${this.config.port}`,
27
+ Server: this.config.options.instanceName ? `${this.config.server}\\${this.config.options.instanceName}` : `${this.config.server},${this.config.port}`,
28
28
  Database: this.config.database,
29
29
  Uid: this.config.user,
30
30
  Pwd: this.config.password,
@@ -4,7 +4,7 @@ const msnodesql = require('msnodesqlv8')
4
4
  const debug = require('debug')('mssql:msv8')
5
5
  const BaseRequest = require('../base/request')
6
6
  const RequestError = require('../error/request-error')
7
- const { IDS, objectHasProperty } = require('../utils')
7
+ const { IDS, objectHasProperty, assertSafeColumnName, assertSafeTypeName, assertSafeTypeSize, assertSafeProcedureName } = require('../utils')
8
8
  const { TYPES, DECLARATIONS, declare } = require('../datatypes')
9
9
  const { PARSERS: UDT } = require('../udt')
10
10
  const Table = require('../table')
@@ -154,16 +154,32 @@ class Request extends BaseRequest {
154
154
 
155
155
  try {
156
156
  table._makeBulk()
157
+ // Checked before a connection is borrowed, so that a rejected name cannot leave
158
+ // the pool short of one.
159
+ table.columns.forEach(col => {
160
+ assertSafeColumnName(col.name)
161
+ // This driver hands the column definition to its table manager rather than writing
162
+ // it into SQL, so a size never reaches a statement here. It is checked all the same,
163
+ // so that a column either works on both drivers or on neither.
164
+ assertSafeTypeSize(col.length)
165
+ assertSafeTypeSize(col.precision)
166
+ assertSafeTypeSize(col.scale)
167
+ })
168
+ // checked here too, so an ordering key is accepted or rejected the same way on
169
+ // either driver even though this one does not build the statement itself
170
+ if (options && options.order) {
171
+ Object.keys(options.order).forEach(key => assertSafeTypeName(key))
172
+ }
157
173
  } catch (e) {
158
174
  return callback(new RequestError(e, 'EREQUEST'))
159
175
  }
160
176
 
161
177
  if (!table.name) {
162
- setImmediate(callback, new RequestError('Table name must be specified for bulk insert.', 'ENAME'))
178
+ return setImmediate(callback, new RequestError('Table name must be specified for bulk insert.', 'ENAME'))
163
179
  }
164
180
 
165
181
  if (table.name.charAt(0) === '@') {
166
- setImmediate(callback, new RequestError("You can't use table variables for bulk insert.", 'ENAME'))
182
+ return setImmediate(callback, new RequestError("You can't use table variables for bulk insert.", 'ENAME'))
167
183
  }
168
184
 
169
185
  this.parent.acquire(this, (err, connection) => {
@@ -189,8 +205,11 @@ class Request extends BaseRequest {
189
205
  connection.hasError = true
190
206
  }
191
207
 
208
+ // a rejected identifier keeps its own code; anything else is a driver
209
+ // error, whose numeric code must not become the public one
210
+ const rejectedIdentifier = err.code === 'EINJECT'
192
211
  err = new RequestError(err)
193
- err.code = 'EREQUEST'
212
+ err.code = rejectedIdentifier ? 'EINJECT' : 'EREQUEST'
194
213
  }
195
214
 
196
215
  this.parent.release(connection)
@@ -210,14 +229,29 @@ class Request extends BaseRequest {
210
229
  }
211
230
 
212
231
  const rows = []
213
- for (const row of Array.from(table.rows)) {
214
- const item = {}
215
- for (let index = 0; index < table.columns.length; index++) {
216
- const col = table.columns[index]
217
- item[col.name] = row[index]
232
+ try {
233
+ for (const col of table.columns) {
234
+ // re-checked here for the same reason as the name below, in case the
235
+ // definition changed since it was checked above
236
+ assertSafeTypeSize(col.length)
237
+ assertSafeTypeSize(col.precision)
238
+ assertSafeTypeSize(col.scale)
218
239
  }
219
240
 
220
- rows.push(item)
241
+ for (const row of Array.from(table.rows)) {
242
+ const item = {}
243
+ for (let index = 0; index < table.columns.length; index++) {
244
+ const col = table.columns[index]
245
+ // this driver hands the name to its table manager rather than writing
246
+ // it into SQL, but it is checked here all the same: a name either
247
+ // works on both drivers or on neither
248
+ item[assertSafeColumnName(col.name)] = row[index]
249
+ }
250
+
251
+ rows.push(item)
252
+ }
253
+ } catch (e) {
254
+ return done(e)
221
255
  }
222
256
 
223
257
  mgr.insertRows(rows, done)
@@ -232,7 +266,16 @@ class Request extends BaseRequest {
232
266
  objectid = table.path
233
267
  }
234
268
 
235
- return connection.queryRaw(`if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`, function (err) {
269
+ // A connection is held from here on, so a column name that changed since it
270
+ // was checked above must be reported through `done`, which releases it.
271
+ let statement
272
+ try {
273
+ statement = `if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`
274
+ } catch (e) {
275
+ return done(e)
276
+ }
277
+
278
+ return connection.queryRaw(statement, function (err) {
236
279
  if (err) { return done(err) }
237
280
  go()
238
281
  })
@@ -314,12 +357,18 @@ class Request extends BaseRequest {
314
357
 
315
358
  if (!this._nested) {
316
359
  const input = []
317
- for (const name in this.parameters) {
318
- if (!objectHasProperty(this.parameters, name)) {
319
- continue
360
+ try {
361
+ for (const name in this.parameters) {
362
+ if (!objectHasProperty(this.parameters, name)) {
363
+ continue
364
+ }
365
+ const param = this.parameters[name]
366
+ // this callback is dispatched on its own tick, so a rejected declaration has
367
+ // to be handed back rather than thrown
368
+ input.push(`@${param.name} ${declare(param.type, param)}`)
320
369
  }
321
- const param = this.parameters[name]
322
- input.push(`@${param.name} ${declare(param.type, param)}`)
370
+ } catch (e) {
371
+ return callback(new RequestError(e, 'EREQUEST'))
323
372
  }
324
373
 
325
374
  const sets = []
@@ -634,14 +683,32 @@ class Request extends BaseRequest {
634
683
  if (err) return callback(err)
635
684
 
636
685
  const params = []
637
- for (const name in this.parameters) {
638
- if (!objectHasProperty(this.parameters, name)) {
639
- continue
640
- }
641
- const param = this.parameters[name]
642
- if (param.io === 2) {
643
- params.push(`@${param.name} ${declare(param.type, param)}`)
686
+ let procedureName
687
+ try {
688
+ // this is built into `exec @___return___ = <name>` below and run as SQL text, so it is
689
+ // checked before it is emitted
690
+ procedureName = assertSafeProcedureName(procedure)
691
+ for (const name in this.parameters) {
692
+ if (!objectHasProperty(this.parameters, name)) {
693
+ continue
694
+ }
695
+ const param = this.parameters[name]
696
+ if (param.io === 2) {
697
+ // this callback is dispatched on its own tick, so a rejected declaration has
698
+ // to be handed back rather than thrown
699
+ params.push(`@${param.name} ${declare(param.type, param)}`)
700
+ } else {
701
+ // An input parameter is bound rather than written into the command, so only an
702
+ // output parameter's size is declared here and nothing would otherwise check an
703
+ // input's. It is checked so that a size is accepted or rejected the same way
704
+ // whichever driver is in use.
705
+ assertSafeTypeSize(param.length)
706
+ assertSafeTypeSize(param.precision)
707
+ assertSafeTypeSize(param.scale)
708
+ }
644
709
  }
710
+ } catch (e) {
711
+ return callback(new RequestError(e, 'EREQUEST'))
645
712
  }
646
713
 
647
714
  // set output params w/ values
@@ -657,7 +724,7 @@ class Request extends BaseRequest {
657
724
  }
658
725
 
659
726
  let cmd = `declare ${['@___return___ int'].concat(params).join(', ')};${sets.join(';')};`
660
- cmd += `exec @___return___ = ${procedure} `
727
+ cmd += `exec @___return___ = ${procedureName} `
661
728
 
662
729
  const spp = []
663
730
  for (const name in this.parameters) {
package/lib/table.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const TYPES = require('./datatypes').TYPES
4
4
  const declareType = require('./datatypes').declare
5
- const objectHasProperty = require('./utils').objectHasProperty
5
+ const { objectHasProperty, assertSafeColumnName } = require('./utils')
6
6
 
7
7
  const MAX = 65535 // (1 << 16) - 1
8
8
  const JSON_COLUMN_ID = 'JSON_F52E2B61-18A1-11d1-B105-00805F49916B'
@@ -99,9 +99,9 @@ Table.prototype._makeBulk = function _makeBulk () {
99
99
  }
100
100
 
101
101
  Table.prototype.declare = function declare () {
102
- const pkey = this.columns.filter(col => col.primary === true).map(col => `[${col.name}]`)
102
+ const pkey = this.columns.filter(col => col.primary === true).map(col => `[${assertSafeColumnName(col.name)}]`)
103
103
  const cols = this.columns.map(col => {
104
- const def = [`[${col.name}] ${declareType(col.type, col)}`]
104
+ const def = [`[${assertSafeColumnName(col.name)}] ${declareType(col.type, col)}`]
105
105
 
106
106
  if (col.nullable === true) {
107
107
  def.push('null')
@@ -4,7 +4,7 @@ const tds = require('tedious')
4
4
  const debug = require('debug')('mssql:tedi')
5
5
  const BaseRequest = require('../base/request')
6
6
  const RequestError = require('../error/request-error')
7
- const { IDS, objectHasProperty } = require('../utils')
7
+ const { IDS, objectHasProperty, assertSafeColumnName, assertSafeTypeName, assertSafeTypeSize, assertSafeProcedureName } = require('../utils')
8
8
  const { TYPES, DECLARATIONS, declare, cast } = require('../datatypes')
9
9
  const Table = require('../table')
10
10
  const { PARSERS: UDT } = require('../udt')
@@ -219,6 +219,15 @@ class Request extends BaseRequest {
219
219
 
220
220
  try {
221
221
  table._makeBulk()
222
+ // Checked before a connection is borrowed, so that a rejected name cannot leave
223
+ // the pool short of one.
224
+ table.columns.forEach(col => assertSafeColumnName(col.name))
225
+ // tedious writes these into `insert bulk ... WITH (ORDER (<key> <direction>))`
226
+ // UNQUOTED — unlike a column name, which it emits as `[name]`. So the quoted-identifier
227
+ // rule does not apply here: what matters is that the key cannot end the clause. It is
228
+ // checked as an identifier, bare or quoted, which is what that position accepts. The
229
+ // checked keys are rebuilt into the options actually handed to the driver, so the value
230
+ // that was validated is the one emitted even if the caller's object changes after.
222
231
  } catch (e) {
223
232
  return callback(new RequestError(e, 'EREQUEST'))
224
233
  }
@@ -332,15 +341,38 @@ class Request extends BaseRequest {
332
341
  }
333
342
  }
334
343
 
335
- const bulk = connection.newBulkLoad(table.path, options, done)
344
+ // A connection is held from here on, so anything that can throw has to hand the
345
+ // error back through `done`, which releases it. That includes newBulkLoad itself:
346
+ // tedious validates the bulk options there and throws for a bad `order` direction,
347
+ // which would otherwise strand the borrowed connection.
348
+ let bulk
349
+ let statement
350
+ try {
351
+ let bulkOptions = options
352
+ if (options && options.order) {
353
+ bulkOptions = Object.assign({}, options, {
354
+ order: Object.fromEntries(Object.entries(options.order)
355
+ .map(([key, direction]) => [assertSafeTypeName(key), direction]))
356
+ })
357
+ }
358
+ bulk = connection.newBulkLoad(table.path, bulkOptions, done)
359
+ for (const col of table.columns) {
360
+ // tedious emits this name as a quoted identifier of its own when it builds
361
+ // the `insert bulk` statement, so it is checked here as well as in
362
+ // Table#declare, in case the name changed since it was checked above.
363
+ bulk.addColumn(assertSafeColumnName(col.name), getTediousType(col.type), { nullable: col.nullable, length: assertSafeTypeSize(col.length), scale: assertSafeTypeSize(col.scale), precision: assertSafeTypeSize(col.precision) })
364
+ }
336
365
 
337
- for (const col of table.columns) {
338
- bulk.addColumn(col.name, getTediousType(col.type), { nullable: col.nullable, length: col.length, scale: col.scale, precision: col.precision })
366
+ if (table.create) {
367
+ const objectid = table.temporary ? `tempdb..[${table.name}]` : table.path
368
+ statement = `if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`
369
+ }
370
+ } catch (e) {
371
+ return done(e)
339
372
  }
340
373
 
341
374
  if (table.create) {
342
- const objectid = table.temporary ? `tempdb..[${table.name}]` : table.path
343
- const req = new tds.Request(`if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`, err => {
375
+ const req = new tds.Request(statement, err => {
344
376
  if (err) return done(err)
345
377
 
346
378
  connection.execBulkLoad(bulk, table.rows)
@@ -439,6 +471,26 @@ class Request extends BaseRequest {
439
471
  connection.on('errorMessage', errorHandlers.errorMessage = handleError.bind(null, false, connection))
440
472
  connection.on('error', errorHandlers.error = handleError.bind(null, true, connection))
441
473
 
474
+ // Give up before the request is sent: the listeners attached just above have to
475
+ // come off the connection before it goes back to the pool, or they accumulate on
476
+ // it and fire for whoever borrows it next.
477
+ const abandon = (err) => {
478
+ delete this._cancel
479
+
480
+ if (hasReturned) {
481
+ return
482
+ }
483
+
484
+ for (const event in errorHandlers) {
485
+ connection.removeListener(event, errorHandlers[event])
486
+ }
487
+
488
+ this.parent.release(connection)
489
+ hasReturned = true
490
+
491
+ return callback(err)
492
+ }
493
+
442
494
  debug('request(%d): query', IDS.get(this), command)
443
495
 
444
496
  const req = new tds.Request(command, err => {
@@ -647,20 +699,23 @@ class Request extends BaseRequest {
647
699
  param.value = getTediousType(param.type).validate(param.value, this.parent.collation)
648
700
  } catch (e) {
649
701
  e.message = `Validation failed for parameter '${name}'. ${e.message}`
650
- const err = new RequestError(e, 'EPARAM')
651
-
652
- this.parent.release(connection)
653
- return callback(err)
702
+ return abandon(new RequestError(e, 'EPARAM'))
654
703
  }
655
704
  }
656
705
 
657
706
  const declarations = []
658
- for (const name in this.parameters) {
659
- if (!objectHasProperty(this.parameters, name)) {
660
- continue
707
+ try {
708
+ for (const name in this.parameters) {
709
+ if (!objectHasProperty(this.parameters, name)) {
710
+ continue
711
+ }
712
+ const param = this.parameters[name]
713
+ // a connection is held here, so a rejected declaration has to be reported
714
+ // through the path that releases it
715
+ declarations.push(`@${name} ${declare(param.type, param)}`)
661
716
  }
662
- const param = this.parameters[name]
663
- declarations.push(`@${name} ${declare(param.type, param)}`)
717
+ } catch (e) {
718
+ return abandon(new RequestError(e, 'EREQUEST'))
664
719
  }
665
720
 
666
721
  const assigns = []
@@ -688,16 +743,22 @@ class Request extends BaseRequest {
688
743
  req.sqlTextOrProcedure = `declare ${declarations.join(', ')};select ${assigns.join(', ')};${req.sqlTextOrProcedure};${batchHasOutput ? (`select 1 as [___return___], ${selects.join(', ')}`) : ''}`
689
744
  }
690
745
  } else {
691
- for (const name in this.parameters) {
692
- if (!objectHasProperty(this.parameters, name)) {
693
- continue
694
- }
695
- const param = this.parameters[name]
696
- if (param.io === 1) {
697
- req.addParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: param.length, scale: param.scale, precision: param.precision })
698
- } else {
699
- req.addOutputParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: param.length, scale: param.scale, precision: param.precision })
746
+ try {
747
+ for (const name in this.parameters) {
748
+ if (!objectHasProperty(this.parameters, name)) {
749
+ continue
750
+ }
751
+ const param = this.parameters[name]
752
+ // tedious builds the sp_executesql declaration from these, so a rejected size
753
+ // has to be reported through the path that releases the connection
754
+ if (param.io === 1) {
755
+ req.addParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: assertSafeTypeSize(param.length), scale: assertSafeTypeSize(param.scale), precision: assertSafeTypeSize(param.precision) })
756
+ } else {
757
+ req.addOutputParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: assertSafeTypeSize(param.length), scale: assertSafeTypeSize(param.scale), precision: assertSafeTypeSize(param.precision) })
758
+ }
700
759
  }
760
+ } catch (e) {
761
+ return abandon(new RequestError(e, 'EREQUEST'))
701
762
  }
702
763
  }
703
764
 
@@ -793,6 +854,35 @@ class Request extends BaseRequest {
793
854
  connection.on('errorMessage', errorHandlers.errorMessage = handleError.bind(null, false, connection))
794
855
  connection.on('error', errorHandlers.error = handleError.bind(null, true, connection))
795
856
 
857
+ // A connection is held from here on. handleError builds a fresh Error and would
858
+ // relabel a rejected identifier as EREQUEST, so anything that can throw here is
859
+ // handed back through this instead, which keeps the code and releases the connection.
860
+ const abandon = (err) => {
861
+ delete this._cancel
862
+
863
+ if (hasReturned) {
864
+ return
865
+ }
866
+
867
+ for (const event in errorHandlers) {
868
+ connection.removeListener(event, errorHandlers[event])
869
+ }
870
+
871
+ this.parent.release(connection)
872
+ hasReturned = true
873
+
874
+ return callback(err)
875
+ }
876
+
877
+ let procedureName
878
+ try {
879
+ procedureName = assertSafeProcedureName(procedure)
880
+ } catch (e) {
881
+ // wrapped like every other rejection here, so the caller sees the same error class
882
+ // whichever driver is in use; MSSQLError keeps the EINJECT code through the wrap
883
+ return abandon(new RequestError(e, 'EREQUEST'))
884
+ }
885
+
796
886
  if (debug.enabled) {
797
887
  // log stored procedure executions and provided parameters
798
888
  const params = Object.keys(this.parameters).map(k => this.parameters[k])
@@ -805,7 +895,7 @@ class Request extends BaseRequest {
805
895
  debug('request(%d): execute %s %O', IDS.get(this), procedure, logParams)
806
896
  }
807
897
 
808
- const req = new tds.Request(procedure, err => {
898
+ const req = new tds.Request(procedureName, err => {
809
899
  // to make sure we handle no-sql errors as well
810
900
  if (err && (!errors.length || (errors.length && err.message !== errors[errors.length - 1].message))) {
811
901
  err = new RequestError(err, 'EREQUEST')
@@ -967,16 +1057,20 @@ class Request extends BaseRequest {
967
1057
  output[parameterName] = value
968
1058
  })
969
1059
 
970
- for (const name in this.parameters) {
971
- if (!objectHasProperty(this.parameters, name)) {
972
- continue
973
- }
974
- const param = this.parameters[name]
975
- if (param.io === 1) {
976
- req.addParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: param.length, scale: param.scale, precision: param.precision })
977
- } else {
978
- req.addOutputParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: param.length, scale: param.scale, precision: param.precision })
1060
+ try {
1061
+ for (const name in this.parameters) {
1062
+ if (!objectHasProperty(this.parameters, name)) {
1063
+ continue
1064
+ }
1065
+ const param = this.parameters[name]
1066
+ if (param.io === 1) {
1067
+ req.addParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: assertSafeTypeSize(param.length), scale: assertSafeTypeSize(param.scale), precision: assertSafeTypeSize(param.precision) })
1068
+ } else {
1069
+ req.addOutputParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: assertSafeTypeSize(param.length), scale: assertSafeTypeSize(param.scale), precision: assertSafeTypeSize(param.precision) })
1070
+ }
979
1071
  }
1072
+ } catch (e) {
1073
+ return abandon(new RequestError(e, 'EREQUEST'))
980
1074
  }
981
1075
 
982
1076
  connection.callProcedure(req)
package/lib/utils.js CHANGED
@@ -1,3 +1,5 @@
1
+ const MSSQLError = require('./error/mssql-error')
2
+
1
3
  const IDS = new WeakMap()
2
4
  const INCREMENT = {
3
5
  Connection: 1,
@@ -7,8 +9,94 @@ const INCREMENT = {
7
9
  PreparedStatement: 1
8
10
  }
9
11
 
12
+ // Parameter names are emitted into generated T-SQL as `@<name>`. The `@` occupies the
13
+ // identifier's first position, so every character of the name sits in a subsequent
14
+ // position, where T-SQL also permits digits and `$`. The set is deliberately wider than
15
+ // the names the server will accept: it excludes the characters that could end the
16
+ // identifier and let the rest of the name be read as SQL, and nothing else, so a name
17
+ // the server dislikes still fails there rather than becoming an error here. The
18
+ // exception is the invisible format characters, which are rejected outright.
19
+ // Only the two zero-width joiners are taken from the format characters; the rest of that
20
+ // category includes ZERO WIDTH SPACE, which the server does treat as a separator.
21
+ const PARAM_NAME = /^(?:[\p{L}\p{N}\p{M}\p{Pc}@#$・・]|\u200C|\u200D)*$/u
22
+ // A table-valued parameter's type name is emitted into the parameter declaration, and a
23
+ // stored procedure's name is emitted into an `exec`, so both are checked the way the server
24
+ // reads a qualified name: one or more parts separated by `.`, each either bare, holding only
25
+ // the characters an identifier is made of, or quoted in brackets or double quotes, where
26
+ // anything goes until the closing mark. Nothing that gets through can end the statement it
27
+ // sits in and have the rest read as SQL.
28
+ const QUALIFIED_NAME_PART = '(?:\\[(?:[^\\]]|\\]\\])*\\]|"(?:[^"]|"")*"|[\\p{L}\\p{N}\\p{M}\\p{Pc}@#$]+)'
29
+ // An intermediate part may be omitted — `db..proc` and `server.db..proc` are ordinary
30
+ // T-SQL — but the first and last parts must be present, so a bare `.`, a leading `.proc`
31
+ // and a trailing `db..` are still refused. An empty part cannot end the identifier.
32
+ const QUALIFIED_NAME = new RegExp(`^(?:${QUALIFIED_NAME_PART}(?:\\.${QUALIFIED_NAME_PART}?)*\\.)?${QUALIFIED_NAME_PART}$`, 'u')
33
+
34
+ // A length, precision or scale is emitted into the same declaration. `max` is the only
35
+ // word the declaration takes there; everything else the server wants is a number.
36
+ const TYPE_SIZE = /^\w*$/
37
+
38
+ // A column name is emitted as a quoted identifier, `[name]`, by this module and again by
39
+ // the driver's own bulk load. Only `]` can terminate that quoting early, and doubling it
40
+ // is the documented way to include one literally.
41
+ const escapesQuotedIdentifier = (name) => String(name).replace(/]]/g, '').includes(']')
42
+
10
43
  module.exports = {
11
44
  objectHasProperty: (object, property) => Object.prototype.hasOwnProperty.call(object, property),
45
+ isValidParameterName: (name) => {
46
+ // Only primitives are accepted: an object could return a different value from a second
47
+ // toString() call, passing validation here and emitting something else into the SQL.
48
+ const type = typeof name
49
+ return (type === 'string' || type === 'number') && PARAM_NAME.test(String(name))
50
+ },
51
+ assertSafeTypeName: (name) => {
52
+ // A type name is optional: the tedious driver takes it from the Table passed as the
53
+ // value, and only uses this one when it is given.
54
+ if (name == null) {
55
+ return name
56
+ }
57
+ // Read once, and return what was read. Whatever is emitted is what was checked, so a
58
+ // value that reports a different name the second time cannot reach the SQL.
59
+ const type = String(name).trim()
60
+ if (!QUALIFIED_NAME.test(type)) {
61
+ throw new MSSQLError(`Invalid type name '${type}'. Type names may only contain identifier characters.`, 'EINJECT')
62
+ }
63
+ return type
64
+ },
65
+ assertSafeProcedureName: (name) => {
66
+ // The msnodesqlv8 driver builds `exec @___return___ = <name>` as SQL text, so the name is
67
+ // checked before it is emitted. The tedious driver sends it as a bound RPC and cannot be
68
+ // injected through it, but it is checked there too so that a name is accepted or rejected
69
+ // the same way whichever driver is in use.
70
+ // Read once, and return what was read, for the same reason as the type name.
71
+ const procedure = String(name).trim()
72
+ if (!QUALIFIED_NAME.test(procedure)) {
73
+ throw new MSSQLError(`Invalid procedure name '${procedure}'. Procedure names may only contain identifier characters.`, 'EINJECT')
74
+ }
75
+ return procedure
76
+ },
77
+ assertSafeTypeSize: (value) => {
78
+ if (value == null || typeof value === 'number') {
79
+ return value
80
+ }
81
+ // Read once and return what was read, for the same reason as the type name. Padding is
82
+ // trimmed rather than refused, and a word the server will reject still reaches it.
83
+ const size = String(value).trim()
84
+ if (!TYPE_SIZE.test(size)) {
85
+ throw new MSSQLError(`Invalid type size '${size}'. A length, precision or scale must be a number.`, 'EINJECT')
86
+ }
87
+ return size
88
+ },
89
+ assertSafeColumnName: (name) => {
90
+ const type = typeof name
91
+ if (type !== 'string' && type !== 'number') {
92
+ throw new MSSQLError(`Invalid column name '${String(name)}'. Column names must be a string or a number.`, 'EINJECT')
93
+ }
94
+ const column = String(name)
95
+ if (escapesQuotedIdentifier(column)) {
96
+ throw new MSSQLError(`Invalid column name '${column}'. A ']' in a column name must be written as ']]'.`, 'EINJECT')
97
+ }
98
+ return column
99
+ },
12
100
  INCREMENT,
13
101
  IDS: {
14
102
  get: IDS.get.bind(IDS),
package/package.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "azure",
22
22
  "node-mssql"
23
23
  ],
24
- "version": "9.2.0",
24
+ "version": "9.2.2",
25
25
  "main": "index.js",
26
26
  "repository": "github:tediousjs/node-mssql",
27
27
  "license": "MIT",
@@ -36,12 +36,12 @@
36
36
  "devDependencies": {
37
37
  "@commitlint/cli": "^17.6.6",
38
38
  "@commitlint/config-conventional": "^17.6.6",
39
- "@semantic-release/commit-analyzer": "^10.0.1",
40
- "@semantic-release/github": "^9.0.3",
41
- "@semantic-release/npm": "^10.0.4",
42
- "@semantic-release/release-notes-generator": "^11.0.4",
39
+ "@semantic-release/commit-analyzer": "^13.0.1",
40
+ "@semantic-release/github": "^12.0.6",
41
+ "@semantic-release/npm": "^13.1.5",
42
+ "@semantic-release/release-notes-generator": "^14.1.0",
43
43
  "mocha": "^10.0.0",
44
- "semantic-release": "^21.0.7",
44
+ "semantic-release": "^25.0.3",
45
45
  "standard": "^17.0.0"
46
46
  },
47
47
  "engines": {