mssql 12.7.1 → 12.7.3

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
@@ -155,6 +155,13 @@ const config = {
155
155
  * [bulk](#bulk-table-options-callback)
156
156
  * [cancel](#cancel)
157
157
 
158
+ ### Identifier rules
159
+
160
+ * [Parameter names](#parameter-names)
161
+ * [Column names](#column-names)
162
+ * [Type names and sizes](#type-names-and-sizes)
163
+ * [Procedure names](#procedure-names)
164
+
158
165
  ### Transactions
159
166
 
160
167
  * [Transaction](#transaction)
@@ -913,6 +920,7 @@ request.execute('procedure_name', (err, result) => {
913
920
 
914
921
  __Errors__
915
922
  - EREQUEST (`RequestError`) - *Message from SQL Server*
923
+ - EINJECT (`RequestError`) - Procedure name or parameter type is not a valid identifier. See [Identifiers](#identifiers).
916
924
  - ECANCEL (`RequestError`) - Cancelled.
917
925
  - ETIMEOUT (`RequestError`) - Request timeout.
918
926
  - ENOCONN (`RequestError`) - No connection is specified for that request.
@@ -972,7 +980,7 @@ sql.map.register(Number, sql.BigInt)
972
980
 
973
981
  __Errors__ (synchronous)
974
982
  - EARGS (`RequestError`) - Invalid number of arguments.
975
- - EINJECT (`RequestError`) - SQL injection warning.
983
+ - EINJECT (`RequestError`) - Parameter name is not a valid identifier. See [Identifiers](#identifiers).
976
984
 
977
985
  ---------------------------------------
978
986
 
@@ -997,7 +1005,7 @@ request.output('output_parameter', sql.VarChar(50), 'abc')
997
1005
 
998
1006
  __Errors__ (synchronous)
999
1007
  - EARGS (`RequestError`) - Invalid number of arguments.
1000
- - EINJECT (`RequestError`) - SQL injection warning.
1008
+ - EINJECT (`RequestError`) - Parameter name is not a valid identifier. See [Identifiers](#identifiers).
1001
1009
 
1002
1010
  ---------------------------------------
1003
1011
 
@@ -1121,6 +1129,7 @@ __Errors__
1121
1129
  - ECONNCLOSED (`ConnectionError`) - Connection is closed.
1122
1130
  - ENOTBEGUN (`TransactionError`) - Transaction has not begun.
1123
1131
  - EABORT (`TransactionError`) - Transaction was aborted (by user or because of an error).
1132
+ - EINJECT (`RequestError`) - Parameter type is not a valid identifier. See [Identifiers](#identifiers).
1124
1133
 
1125
1134
  ```javascript
1126
1135
  const request = new sql.Request()
@@ -1166,6 +1175,7 @@ __Errors__
1166
1175
  - ECONNCLOSED (`ConnectionError`) - Connection is closed.
1167
1176
  - ENOTBEGUN (`TransactionError`) - Transaction has not begun.
1168
1177
  - EABORT (`TransactionError`) - Transaction was aborted (by user or because of an error).
1178
+ - EINJECT (`RequestError`) - Parameter type is not a valid identifier. See [Identifiers](#identifiers).
1169
1179
 
1170
1180
  ---------------------------------------
1171
1181
 
@@ -1210,6 +1220,7 @@ __Errors__
1210
1220
  - ECONNCLOSED (`ConnectionError`) - Connection is closed.
1211
1221
  - ENOTBEGUN (`TransactionError`) - Transaction has not begun.
1212
1222
  - EABORT (`TransactionError`) - Transaction was aborted (by user or because of an error).
1223
+ - EINJECT (`RequestError`) - Column name or parameter type is not a valid identifier. See [Identifiers](#identifiers).
1213
1224
 
1214
1225
  ---------------------------------------
1215
1226
 
@@ -1444,7 +1455,7 @@ ps.input('input_parameter', sql.VarChar(50))
1444
1455
 
1445
1456
  __Errors__ (synchronous)
1446
1457
  - EARGS (`PreparedStatementError`) - Invalid number of arguments.
1447
- - EINJECT (`PreparedStatementError`) - SQL injection warning.
1458
+ - EINJECT (`PreparedStatementError`) - Parameter name is not a valid identifier. See [Identifiers](#identifiers).
1448
1459
 
1449
1460
  ---------------------------------------
1450
1461
 
@@ -1466,7 +1477,7 @@ ps.output('output_parameter', sql.VarChar(50))
1466
1477
 
1467
1478
  __Errors__ (synchronous)
1468
1479
  - EARGS (`PreparedStatementError`) - Invalid number of arguments.
1469
- - EINJECT (`PreparedStatementError`) - SQL injection warning.
1480
+ - EINJECT (`PreparedStatementError`) - Parameter name is not a valid identifier. See [Identifiers](#identifiers).
1470
1481
 
1471
1482
  ---------------------------------------
1472
1483
 
@@ -2039,6 +2050,100 @@ Results in:
2039
2050
  ]
2040
2051
  ```
2041
2052
 
2053
+ ## Identifiers
2054
+
2055
+ Values are sent to the server as parameters and may hold anything. A few things
2056
+ are not values, and cannot be: the name of a parameter, the name of a column in a
2057
+ bulk load, the type a parameter is declared as, and the name of a stored procedure.
2058
+ Those are *identifiers*, and
2059
+ no SQL database can bind them, so this library builds them into the statement it
2060
+ sends. It checks them, and rejects anything that could end the identifier and have
2061
+ the rest of the value read as SQL.
2062
+
2063
+ That check is not a substitute for your own. If any of these comes from somewhere
2064
+ you do not control, map it to a value you expect first:
2065
+
2066
+ ```javascript
2067
+ const columns = { name: 'name', created: 'created_at' } // what you allow
2068
+ const column = columns[req.query.sortBy] // what they asked for
2069
+ if (!column) throw new Error('unknown column')
2070
+
2071
+ request.input(column, sql.NVarChar, value)
2072
+ ```
2073
+
2074
+ ### Parameter names
2075
+
2076
+ `request.input()`, `request.output()` and the same methods on a prepared statement
2077
+ take a name without the leading `@`. It may hold letters, digits, marks and
2078
+ connector punctuation from any script, along with `@`, `#`, `$` and the two
2079
+ zero-width joiners. A name that is not a string or a number is rejected, as is one
2080
+ holding an invisible format character, because one of them is a separator the
2081
+ server treats as ending the identifier.
2082
+
2083
+ Anything else raises `EINJECT`. Note that a name this library accepts may still be
2084
+ one the server rejects; the check is deliberately wider than the server's own rules
2085
+ so that it only refuses what could change the shape of the statement.
2086
+
2087
+ ### Column names
2088
+
2089
+ `table.columns.add()` names a column in a bulk load, and the name is emitted as a
2090
+ quoted identifier. A `]` ends that quoting, so a name holding one must double it,
2091
+ which is how T-SQL escapes it:
2092
+
2093
+ ```javascript
2094
+ table.columns.add('a]]b', sql.Int) // the column named a]b
2095
+ ```
2096
+
2097
+ A single `]` raises `EINJECT`. Column names are not otherwise restricted: spaces,
2098
+ punctuation and reserved words are all fine, because the quoting handles them.
2099
+
2100
+ The keys of `options.order` are also identifiers, but the driver writes them into the bulk
2101
+ statement's `ORDER (...)` clause *unquoted* rather than as `[name]`. They follow the type
2102
+ name rule instead: bare, or quoted with brackets or double quotes.
2103
+
2104
+ ```javascript
2105
+ request.bulk(table, { order: { created_at: 'ASC' } })
2106
+ request.bulk(table, { order: { '[Created At]': 'DESC' } }) // quote a spaced name yourself
2107
+ ```
2108
+
2109
+ ### Type names and sizes
2110
+
2111
+ `sql.TVP()` takes the name of a table type, optionally qualified with a schema and
2112
+ optionally quoted with brackets or double quotes. A size — the length, precision or
2113
+ scale of a type such as `sql.VarChar()` — must be a number, or the word `max` the
2114
+ declaration accepts in its place. Padding is trimmed.
2115
+
2116
+ ```javascript
2117
+ request.input('rows', sql.TVP('dbo.[My Type]'), table)
2118
+ request.input('name', sql.VarChar(50), value)
2119
+ request.input('note', sql.VarChar('max'), value)
2120
+ ```
2121
+
2122
+ A size is checked wherever it is built into SQL, which includes the declaration a
2123
+ bulk load sends and the parameter list a query sends, not only `sql.TVP()`.
2124
+
2125
+ ### Procedure names
2126
+
2127
+ `request.execute()` takes the name of a stored procedure, optionally qualified with
2128
+ a server, database and schema and optionally quoted with brackets or double quotes.
2129
+ An omitted part may be left empty, as T-SQL allows. The `msnodesqlv8` driver builds
2130
+ this into the statement it sends; the `tedious` driver sends it as a bound remote
2131
+ procedure call and could not be injected through it. Both check it, so a name is
2132
+ accepted or rejected the same way whichever driver you use.
2133
+
2134
+ ```javascript
2135
+ request.execute('dbo.[My Procedure]')
2136
+ request.execute('master..sp_who') // an omitted schema is fine
2137
+ ```
2138
+
2139
+ The first and last parts must be present, so `.proc` and `db..` are rejected.
2140
+
2141
+ Anything else raises `EINJECT`. Numbered procedures — the deprecated
2142
+ `procedure;1` form — are rejected, because the `;` cannot be told apart from the
2143
+ end of a statement.
2144
+
2145
+ ---------------------------------------
2146
+
2042
2147
  ## Errors
2043
2148
 
2044
2149
  There are 4 types of errors you can handle:
@@ -2075,10 +2180,10 @@ Name | Code | Message
2075
2180
  `RequestError` | ECANCEL | Cancelled.
2076
2181
  `RequestError` | ETIMEOUT | Request timeout.
2077
2182
  `RequestError` | EARGS | Invalid number of arguments.
2078
- `RequestError` | EINJECT | SQL injection warning.
2183
+ `RequestError` | EINJECT | Parameter name, column name or type is not a valid identifier.
2079
2184
  `RequestError` | ENOCONN | No connection is specified for that request.
2080
2185
  `PreparedStatementError` | EARGS | Invalid number of arguments.
2081
- `PreparedStatementError` | EINJECT | SQL injection warning.
2186
+ `PreparedStatementError` | EINJECT | Parameter name is not a valid identifier.
2082
2187
  `PreparedStatementError` | EALREADYPREPARED | Statement is already prepared.
2083
2188
  `PreparedStatementError` | ENOTPREPARED | Statement is not prepared.
2084
2189
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const debug = require('debug')('mssql:base')
4
4
  const { EventEmitter } = require('node:events')
5
- const { IDS, objectHasProperty, getPoolId } = require('../utils')
5
+ const { IDS, objectHasProperty, getPoolId, isValidParameterName } = require('../utils')
6
6
  const globalConnection = require('../global-connection')
7
7
  const { TransactionError, PreparedStatementError } = require('../error')
8
8
  const shared = require('../shared')
@@ -97,8 +97,8 @@ class PreparedStatement extends EventEmitter {
97
97
  */
98
98
 
99
99
  input (name, type) {
100
- if (/--| |\/\*|\*\/|'/.test(name)) {
101
- throw new PreparedStatementError(`SQL injection warning for param '${name}'`, 'EINJECT')
100
+ if (!isValidParameterName(name)) {
101
+ throw new PreparedStatementError(`Invalid parameter name '${name}'. Parameter names may only contain identifier characters`, 'EINJECT')
102
102
  }
103
103
 
104
104
  if (arguments.length < 2) {
@@ -150,8 +150,8 @@ class PreparedStatement extends EventEmitter {
150
150
  */
151
151
 
152
152
  output (name, type) {
153
- if (/--| |\/\*|\*\/|'/.test(name)) {
154
- throw new PreparedStatementError(`SQL injection warning for param '${name}'`, 'EINJECT')
153
+ if (!isValidParameterName(name)) {
154
+ throw new PreparedStatementError(`Invalid parameter name '${name}'. Parameter names may only contain identifier characters`, 'EINJECT')
155
155
  }
156
156
 
157
157
  if (arguments.length < 2) {
@@ -254,17 +254,25 @@ class PreparedStatement extends EventEmitter {
254
254
  req._internal = true
255
255
  req.stream = false
256
256
  req.output('handle', TYPES.Int)
257
- req.input('params', TYPES.NVarChar, ((() => {
258
- const result = []
257
+ let params
258
+ try {
259
+ params = []
259
260
  for (const name in this.parameters) {
260
261
  if (!objectHasProperty(this.parameters, name)) {
261
262
  continue
262
263
  }
263
264
  const param = this.parameters[name]
264
- result.push(`@${name} ${declare(param.type, param)}${param.io === 2 ? ' output' : ''}`)
265
+ // a connection is held here, so a rejected declaration has to be reported
266
+ // through the path that releases it
267
+ params.push(`@${name} ${declare(param.type, param)}${param.io === 2 ? ' output' : ''}`)
265
268
  }
266
- return result
267
- })()).join(','))
269
+ } catch (e) {
270
+ this.parent.release(this._acquiredConnection)
271
+ this._acquiredConnection = null
272
+ this._acquiredConfig = null
273
+ return callback(new PreparedStatementError(e, 'EREQUEST'))
274
+ }
275
+ req.input('params', TYPES.NVarChar, params.join(','))
268
276
  req.input('stmt', TYPES.NVarChar, this.statement)
269
277
  req.execute('sp_prepare', (err, result) => {
270
278
  if (err) {
@@ -3,7 +3,7 @@
3
3
  const debug = require('debug')('mssql:base')
4
4
  const { EventEmitter } = require('node:events')
5
5
  const { Readable } = require('node:stream')
6
- const { IDS, objectHasProperty, getPoolId } = require('../utils')
6
+ const { IDS, objectHasProperty, getPoolId, isValidParameterName } = require('../utils')
7
7
  const globalConnection = require('../global-connection')
8
8
  const { RequestError, ConnectionError } = require('../error')
9
9
  const { TYPES } = require('../datatypes')
@@ -115,8 +115,8 @@ class Request extends EventEmitter {
115
115
  */
116
116
 
117
117
  input (name, type, value) {
118
- if (/--| |\/\*|\*\/|'/.test(name)) {
119
- throw new RequestError(`SQL injection warning for param '${name}'`, 'EINJECT')
118
+ if (!isValidParameterName(name)) {
119
+ throw new RequestError(`Invalid parameter name '${name}'. Parameter names may only contain identifier characters`, 'EINJECT')
120
120
  }
121
121
 
122
122
  if (arguments.length < 2) {
@@ -178,8 +178,8 @@ class Request extends EventEmitter {
178
178
  output (name, type, value) {
179
179
  if (!type) { type = TYPES.NVarChar }
180
180
 
181
- if (/--| |\/\*|\*\/|'/.test(name)) {
182
- throw new RequestError(`SQL injection warning for param '${name}'`, 'EINJECT')
181
+ if (!isValidParameterName(name)) {
182
+ throw new RequestError(`Invalid parameter name '${name}'. Parameter names may only contain identifier characters`, 'EINJECT')
183
183
  }
184
184
 
185
185
  if ((type === TYPES.Text) || (type === TYPES.NText) || (type === TYPES.Image)) {
package/lib/datatypes.js CHANGED
@@ -1,6 +1,5 @@
1
1
  'use strict'
2
-
3
- const objectHasProperty = require('./utils').objectHasProperty
2
+ const { objectHasProperty, assertSafeTypeName, assertSafeTypeSize } = require('./utils')
4
3
  const inspect = Symbol.for('nodejs.util.inspect.custom')
5
4
 
6
5
  const TYPES = {
@@ -133,19 +132,33 @@ for (const key in TYPES) {
133
132
  }
134
133
 
135
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.
136
138
  switch (type) {
137
- case TYPES.VarChar: case TYPES.VarBinary:
138
- return `${type.declaration} (${options.length > 8000 ? 'MAX' : (options.length == null ? 'MAX' : options.length)})`
139
- case TYPES.NVarChar:
140
- return `${type.declaration} (${options.length > 4000 ? 'MAX' : (options.length == null ? 'MAX' : options.length)})`
141
- case TYPES.Char: case TYPES.NChar: case TYPES.Binary:
142
- return `${type.declaration} (${options.length == null ? 1 : options.length})`
143
- case TYPES.Decimal: case TYPES.Numeric:
144
- return `${type.declaration} (${options.precision == null ? 18 : options.precision}, ${options.scale == null ? 0 : options.scale})`
145
- case TYPES.Time: case TYPES.DateTime2: case TYPES.DateTimeOffset:
146
- 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
+ }
147
160
  case TYPES.TVP:
148
- return `${options.tvpType} readonly`
161
+ return `${assertSafeTypeName(options.tvpType)} readonly`
149
162
  default:
150
163
  return type.declaration
151
164
  }
@@ -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')
@@ -160,16 +160,32 @@ class Request extends BaseRequest {
160
160
 
161
161
  try {
162
162
  table._makeBulk()
163
+ // Checked before a connection is borrowed, so that a rejected name cannot leave
164
+ // the pool short of one.
165
+ table.columns.forEach(col => {
166
+ assertSafeColumnName(col.name)
167
+ // This driver hands the column definition to its table manager rather than writing
168
+ // it into SQL, so a size never reaches a statement here. It is checked all the same,
169
+ // so that a column either works on both drivers or on neither.
170
+ assertSafeTypeSize(col.length)
171
+ assertSafeTypeSize(col.precision)
172
+ assertSafeTypeSize(col.scale)
173
+ })
174
+ // checked here too, so an ordering key is accepted or rejected the same way on
175
+ // either driver even though this one does not build the statement itself
176
+ if (options && options.order) {
177
+ Object.keys(options.order).forEach(key => assertSafeTypeName(key))
178
+ }
163
179
  } catch (e) {
164
180
  return callback(new RequestError(e, 'EREQUEST'))
165
181
  }
166
182
 
167
183
  if (!table.name) {
168
- setImmediate(callback, new RequestError('Table name must be specified for bulk insert.', 'ENAME'))
184
+ return setImmediate(callback, new RequestError('Table name must be specified for bulk insert.', 'ENAME'))
169
185
  }
170
186
 
171
187
  if (table.name.charAt(0) === '@') {
172
- setImmediate(callback, new RequestError("You can't use table variables for bulk insert.", 'ENAME'))
188
+ return setImmediate(callback, new RequestError("You can't use table variables for bulk insert.", 'ENAME'))
173
189
  }
174
190
 
175
191
  this.parent.acquire(this, (err, connection, config) => {
@@ -201,8 +217,11 @@ class Request extends BaseRequest {
201
217
  connection.hasError = true
202
218
  }
203
219
 
220
+ // a rejected identifier keeps its own code; anything else is a driver
221
+ // error, whose numeric code must not become the public one
222
+ const rejectedIdentifier = err.code === 'EINJECT'
204
223
  err = new RequestError(err)
205
- err.code = 'EREQUEST'
224
+ err.code = rejectedIdentifier ? 'EINJECT' : 'EREQUEST'
206
225
  }
207
226
 
208
227
  this.parent.release(connection)
@@ -222,14 +241,29 @@ class Request extends BaseRequest {
222
241
  }
223
242
 
224
243
  const rows = []
225
- for (const row of Array.from(table.rows)) {
226
- const item = {}
227
- for (let index = 0; index < table.columns.length; index++) {
228
- const col = table.columns[index]
229
- item[col.name] = row[index]
244
+ try {
245
+ for (const col of table.columns) {
246
+ // re-checked here for the same reason as the name below, in case the
247
+ // definition changed since it was checked above
248
+ assertSafeTypeSize(col.length)
249
+ assertSafeTypeSize(col.precision)
250
+ assertSafeTypeSize(col.scale)
230
251
  }
231
252
 
232
- rows.push(item)
253
+ for (const row of Array.from(table.rows)) {
254
+ const item = {}
255
+ for (let index = 0; index < table.columns.length; index++) {
256
+ const col = table.columns[index]
257
+ // this driver hands the name to its table manager rather than writing
258
+ // it into SQL, but it is checked here all the same: a name either
259
+ // works on both drivers or on neither
260
+ item[assertSafeColumnName(col.name)] = row[index]
261
+ }
262
+
263
+ rows.push(item)
264
+ }
265
+ } catch (e) {
266
+ return done(e)
233
267
  }
234
268
 
235
269
  mgr.insertRows(rows, done)
@@ -244,10 +278,19 @@ class Request extends BaseRequest {
244
278
  objectid = table.path
245
279
  }
246
280
 
247
- return connection.queryRaw({
248
- query_str: `if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`,
249
- query_timeout: (this.overrides.requestTimeout ?? config.requestTimeout) / 1000 // msnodesqlv8 timeouts are in seconds (<1 second not supported)
250
- }, function (err) {
281
+ // A connection is held from here on, so a column name that changed since it
282
+ // was checked above must be reported through `done`, which releases it.
283
+ let statement
284
+ try {
285
+ statement = {
286
+ query_str: `if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`,
287
+ query_timeout: (this.overrides.requestTimeout ?? config.requestTimeout) / 1000 // msnodesqlv8 timeouts are in seconds (<1 second not supported)
288
+ }
289
+ } catch (e) {
290
+ return done(e)
291
+ }
292
+
293
+ return connection.queryRaw(statement, function (err) {
251
294
  if (err) { return done(err) }
252
295
  go()
253
296
  })
@@ -259,7 +302,7 @@ class Request extends BaseRequest {
259
302
  })
260
303
  }
261
304
 
262
- _query (command, callback) {
305
+ _query (command, callback, bound) {
263
306
  super._query(command, err => {
264
307
  if (err) return callback(err)
265
308
 
@@ -329,12 +372,18 @@ class Request extends BaseRequest {
329
372
 
330
373
  if (!this._nested) {
331
374
  const input = []
332
- for (const name in this.parameters) {
333
- if (!objectHasProperty(this.parameters, name)) {
334
- continue
375
+ try {
376
+ for (const name in this.parameters) {
377
+ if (!objectHasProperty(this.parameters, name)) {
378
+ continue
379
+ }
380
+ const param = this.parameters[name]
381
+ // this callback is dispatched on its own tick, so a rejected declaration has
382
+ // to be handed back rather than thrown
383
+ input.push(`@${param.name} ${declare(param.type, param)}`)
335
384
  }
336
- const param = this.parameters[name]
337
- input.push(`@${param.name} ${declare(param.type, param)}`)
385
+ } catch (e) {
386
+ return callback(new RequestError(e, 'EREQUEST'))
338
387
  }
339
388
 
340
389
  const sets = []
@@ -343,7 +392,9 @@ class Request extends BaseRequest {
343
392
  continue
344
393
  }
345
394
  const param = this.parameters[name]
346
- if (param.io === 1) {
395
+ // an output's initial value is bound with the inputs below, so it needs a marker too;
396
+ // an output given no value is null, and 0, '' and false are values like any other
397
+ if (param.io === 1 || (param.io === 2 && param.value != null)) {
347
398
  sets.push(`set @${param.name}=?`)
348
399
  }
349
400
  }
@@ -377,15 +428,23 @@ class Request extends BaseRequest {
377
428
  return callback(new RequestError('Canceled.', 'ECANCEL'))
378
429
  }
379
430
 
431
+ // values are bound to the markers by position, so a caller that writes its own
432
+ // markers passes the parameters in the order it wrote them
380
433
  const params = []
381
- for (const name in this.parameters) {
382
- if (!objectHasProperty(this.parameters, name)) {
383
- continue
384
- }
385
- const param = this.parameters[name]
386
- if (param.io === 1 || (param.io === 2 && param.value)) {
434
+ if (bound) {
435
+ for (const param of bound) {
387
436
  params.push(castParameter(param.value, param.type))
388
437
  }
438
+ } else {
439
+ for (const name in this.parameters) {
440
+ if (!objectHasProperty(this.parameters, name)) {
441
+ continue
442
+ }
443
+ const param = this.parameters[name]
444
+ if (param.io === 1 || (param.io === 2 && param.value != null)) {
445
+ params.push(castParameter(param.value, param.type))
446
+ }
447
+ }
389
448
  }
390
449
 
391
450
  debug('request(%d): query', IDS.get(this), command)
@@ -649,16 +708,38 @@ class Request extends BaseRequest {
649
708
  if (err) return callback(err)
650
709
 
651
710
  const params = []
652
- for (const name in this.parameters) {
653
- if (!objectHasProperty(this.parameters, name)) {
654
- continue
655
- }
656
- const param = this.parameters[name]
657
- if (param.io === 2) {
658
- params.push(`@${param.name} ${declare(param.type, param)}`)
711
+ let procedureName
712
+ try {
713
+ // this is built into `exec @___return___ = <name>` below and run as SQL text, so it is
714
+ // checked before it is emitted
715
+ procedureName = assertSafeProcedureName(procedure)
716
+ for (const name in this.parameters) {
717
+ if (!objectHasProperty(this.parameters, name)) {
718
+ continue
719
+ }
720
+ const param = this.parameters[name]
721
+ if (param.io === 2) {
722
+ // this callback is dispatched on its own tick, so a rejected declaration has
723
+ // to be handed back rather than thrown
724
+ params.push(`@${param.name} ${declare(param.type, param)}`)
725
+ } else {
726
+ // An input parameter is bound rather than written into the command, so only an
727
+ // output parameter's size is declared here and nothing would otherwise check an
728
+ // input's. It is checked so that a size is accepted or rejected the same way
729
+ // whichever driver is in use.
730
+ assertSafeTypeSize(param.length)
731
+ assertSafeTypeSize(param.precision)
732
+ assertSafeTypeSize(param.scale)
733
+ }
659
734
  }
735
+ } catch (e) {
736
+ return callback(new RequestError(e, 'EREQUEST'))
660
737
  }
661
738
 
739
+ // the parameters in the order their markers are written: output initial values are set
740
+ // before the call and inputs are passed to it, whatever order they were declared in
741
+ const bound = []
742
+
662
743
  // set output params w/ values
663
744
  const sets = []
664
745
  for (const name in this.parameters) {
@@ -666,13 +747,14 @@ class Request extends BaseRequest {
666
747
  continue
667
748
  }
668
749
  const param = this.parameters[name]
669
- if (param.io === 2 && param.value) {
750
+ if (param.io === 2 && param.value != null) {
670
751
  sets.push(`set @${param.name}=?`)
752
+ bound.push(param)
671
753
  }
672
754
  }
673
755
 
674
756
  let cmd = `declare ${['@___return___ int'].concat(params).join(', ')};${sets.join(';')};`
675
- cmd += `exec @___return___ = ${procedure} `
757
+ cmd += `exec @___return___ = ${procedureName} `
676
758
 
677
759
  const spp = []
678
760
  for (const name in this.parameters) {
@@ -687,6 +769,7 @@ class Request extends BaseRequest {
687
769
  } else {
688
770
  // input parameter
689
771
  spp.push(`@${param.name}=?`)
772
+ bound.push(param)
690
773
  }
691
774
  }
692
775
 
@@ -762,7 +845,7 @@ class Request extends BaseRequest {
762
845
  } else {
763
846
  callback(null, recordsets, output, returnValue, rowsAffected, recordsetcolumns)
764
847
  }
765
- })
848
+ }, bound)
766
849
  })
767
850
  }
768
851
 
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')
@@ -12,6 +12,7 @@ const { valueHandler } = require('../shared')
12
12
 
13
13
  const JSON_COLUMN_ID = 'JSON_F52E2B61-18A1-11d1-B105-00805F49916B'
14
14
  const XML_COLUMN_ID = 'XML_F52E2B61-18A1-11d1-B105-00805F49916B'
15
+ const MAX = 65535 // (1 << 16) - 1
15
16
 
16
17
  const N_TYPES = {
17
18
  BitN: 0x68,
@@ -174,6 +175,18 @@ const valueCorrection = function (value, metadata) {
174
175
  }
175
176
  }
176
177
 
178
+ // tedious only compares a length as a number, so `max` is passed as the length it stands
179
+ // for. As a word it compares false against the type's maximum both ways, and tedious then
180
+ // writes a `max` value without its terminator.
181
+ const tediousLength = function (length) {
182
+ return typeof length === 'string' && length.trim().toLowerCase() === 'max' ? MAX : length
183
+ }
184
+
185
+ // tedious writes a TVP column with the length it is given, without resolving a missing one,
186
+ // and a missing length fails the same comparisons as `max`. These types are given `max`,
187
+ // which is how tedious wrote them before it compared the length both ways.
188
+ const MAX_LENGTH_TYPES = new Set([tds.TYPES.NVarChar, tds.TYPES.VarChar, tds.TYPES.VarBinary])
189
+
177
190
  const parameterCorrection = function (value) {
178
191
  if (value instanceof Table) {
179
192
  const tvp = {
@@ -191,7 +204,7 @@ const parameterCorrection = function (value) {
191
204
  tvp.columns.push({
192
205
  name: col.name,
193
206
  type: tediousType,
194
- length: col.length,
207
+ length: col.length == null && MAX_LENGTH_TYPES.has(tediousType) ? MAX : tediousLength(col.length),
195
208
  scale: col.scale,
196
209
  precision: col.precision
197
210
  })
@@ -223,6 +236,15 @@ class Request extends BaseRequest {
223
236
 
224
237
  try {
225
238
  table._makeBulk()
239
+ // Checked before a connection is borrowed, so that a rejected name cannot leave
240
+ // the pool short of one.
241
+ table.columns.forEach(col => assertSafeColumnName(col.name))
242
+ // tedious writes these into `insert bulk ... WITH (ORDER (<key> <direction>))`
243
+ // UNQUOTED — unlike a column name, which it emits as `[name]`. So the quoted-identifier
244
+ // rule does not apply here: what matters is that the key cannot end the clause. It is
245
+ // checked as an identifier, bare or quoted, which is what that position accepts. The
246
+ // checked keys are rebuilt into the options actually handed to the driver, so the value
247
+ // that was validated is the one emitted even if the caller's object changes after.
226
248
  } catch (e) {
227
249
  return callback(new RequestError(e, 'EREQUEST'))
228
250
  }
@@ -340,15 +362,38 @@ class Request extends BaseRequest {
340
362
  }
341
363
  }
342
364
 
343
- const bulk = connection.newBulkLoad(table.path, options, done)
365
+ // A connection is held from here on, so anything that can throw has to hand the
366
+ // error back through `done`, which releases it. That includes newBulkLoad itself:
367
+ // tedious validates the bulk options there and throws for a bad `order` direction,
368
+ // which would otherwise strand the borrowed connection.
369
+ let bulk
370
+ let statement
371
+ try {
372
+ let bulkOptions = options
373
+ if (options && options.order) {
374
+ bulkOptions = Object.assign({}, options, {
375
+ order: Object.fromEntries(Object.entries(options.order)
376
+ .map(([key, direction]) => [assertSafeTypeName(key), direction]))
377
+ })
378
+ }
379
+ bulk = connection.newBulkLoad(table.path, bulkOptions, done)
380
+ for (const col of table.columns) {
381
+ // tedious emits this name as a quoted identifier of its own when it builds
382
+ // the `insert bulk` statement, so it is checked here as well as in
383
+ // Table#declare, in case the name changed since it was checked above.
384
+ bulk.addColumn(assertSafeColumnName(col.name), getTediousType(col.type), { nullable: col.nullable, length: tediousLength(assertSafeTypeSize(col.length)), scale: assertSafeTypeSize(col.scale), precision: assertSafeTypeSize(col.precision) })
385
+ }
344
386
 
345
- for (const col of table.columns) {
346
- bulk.addColumn(col.name, getTediousType(col.type), { nullable: col.nullable, length: col.length, scale: col.scale, precision: col.precision })
387
+ if (table.create) {
388
+ const objectid = table.temporary ? `tempdb..[${table.name}]` : table.path
389
+ statement = `if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`
390
+ }
391
+ } catch (e) {
392
+ return done(e)
347
393
  }
348
394
 
349
395
  if (table.create) {
350
- const objectid = table.temporary ? `tempdb..[${table.name}]` : table.path
351
- const req = new tds.Request(`if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`, err => {
396
+ const req = new tds.Request(statement, err => {
352
397
  if (err) return done(err)
353
398
 
354
399
  connection.execBulkLoad(bulk, table.rows)
@@ -450,6 +495,26 @@ class Request extends BaseRequest {
450
495
  connection.on('errorMessage', errorHandlers.errorMessage = handleError.bind(null, false, connection))
451
496
  connection.on('error', errorHandlers.error = handleError.bind(null, true, connection))
452
497
 
498
+ // Give up before the request is sent: the listeners attached just above have to
499
+ // come off the connection before it goes back to the pool, or they accumulate on
500
+ // it and fire for whoever borrows it next.
501
+ const abandon = (err) => {
502
+ delete this._cancel
503
+
504
+ if (hasReturned) {
505
+ return
506
+ }
507
+
508
+ for (const event in errorHandlers) {
509
+ connection.removeListener(event, errorHandlers[event])
510
+ }
511
+
512
+ this.parent.release(connection)
513
+ hasReturned = true
514
+
515
+ return callback(err)
516
+ }
517
+
453
518
  debug('request(%d): query', IDS.get(this), command)
454
519
 
455
520
  const req = new tds.Request(command, err => {
@@ -666,29 +731,23 @@ class Request extends BaseRequest {
666
731
  param.value = getTediousType(param.type).validate(param.value, this.parent.collation)
667
732
  } catch (e) {
668
733
  e.message = `Validation failed for parameter '${name}'. ${e.message}`
669
- const err = new RequestError(e, 'EPARAM')
670
- delete this._cancel
671
-
672
- if (!hasReturned) {
673
- for (const event in errorHandlers) {
674
- connection.removeListener(event, errorHandlers[event])
675
- }
676
-
677
- this.parent.release(connection)
678
- hasReturned = true
679
- return callback(err)
680
- }
681
- return
734
+ return abandon(new RequestError(e, 'EPARAM'))
682
735
  }
683
736
  }
684
737
 
685
738
  const declarations = []
686
- for (const name in this.parameters) {
687
- if (!objectHasProperty(this.parameters, name)) {
688
- continue
739
+ try {
740
+ for (const name in this.parameters) {
741
+ if (!objectHasProperty(this.parameters, name)) {
742
+ continue
743
+ }
744
+ const param = this.parameters[name]
745
+ // a connection is held here, so a rejected declaration has to be reported
746
+ // through the path that releases it
747
+ declarations.push(`@${name} ${declare(param.type, param)}`)
689
748
  }
690
- const param = this.parameters[name]
691
- declarations.push(`@${name} ${declare(param.type, param)}`)
749
+ } catch (e) {
750
+ return abandon(new RequestError(e, 'EREQUEST'))
692
751
  }
693
752
 
694
753
  const assigns = []
@@ -724,14 +783,20 @@ class Request extends BaseRequest {
724
783
  continue
725
784
  }
726
785
  const param = this.parameters[name]
786
+ // tedious builds the sp_executesql declaration from these, so a rejected size
787
+ // has to be reported through the path that releases the connection
727
788
  if (param.io === 1) {
728
- req.addParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: param.length, scale: param.scale, precision: param.precision })
789
+ req.addParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: tediousLength(assertSafeTypeSize(param.length)), scale: assertSafeTypeSize(param.scale), precision: assertSafeTypeSize(param.precision) })
729
790
  } else {
730
- req.addOutputParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: param.length, scale: param.scale, precision: param.precision })
791
+ req.addOutputParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: tediousLength(assertSafeTypeSize(param.length)), scale: assertSafeTypeSize(param.scale), precision: assertSafeTypeSize(param.precision) })
731
792
  }
732
793
  }
733
794
  }
795
+ } catch (e) {
796
+ return abandon(new RequestError(e, 'EREQUEST'))
797
+ }
734
798
 
799
+ try {
735
800
  connection[this._isBatch ? 'execSqlBatch' : 'execSql'](req)
736
801
  } catch (error) {
737
802
  handleError(true, connection, error)
@@ -823,6 +888,35 @@ class Request extends BaseRequest {
823
888
  connection.on('errorMessage', errorHandlers.errorMessage = handleError.bind(null, false, connection))
824
889
  connection.on('error', errorHandlers.error = handleError.bind(null, true, connection))
825
890
 
891
+ // A connection is held from here on. handleError builds a fresh Error and would
892
+ // relabel a rejected identifier as EREQUEST, so anything that can throw here is
893
+ // handed back through this instead, which keeps the code and releases the connection.
894
+ const abandon = (err) => {
895
+ delete this._cancel
896
+
897
+ if (hasReturned) {
898
+ return
899
+ }
900
+
901
+ for (const event in errorHandlers) {
902
+ connection.removeListener(event, errorHandlers[event])
903
+ }
904
+
905
+ this.parent.release(connection)
906
+ hasReturned = true
907
+
908
+ return callback(err)
909
+ }
910
+
911
+ let procedureName
912
+ try {
913
+ procedureName = assertSafeProcedureName(procedure)
914
+ } catch (e) {
915
+ // wrapped like every other rejection here, so the caller sees the same error class
916
+ // whichever driver is in use; MSSQLError keeps the EINJECT code through the wrap
917
+ return abandon(new RequestError(e, 'EREQUEST'))
918
+ }
919
+
826
920
  if (debug.enabled) {
827
921
  // log stored procedure executions and provided parameters
828
922
  const params = Object.keys(this.parameters).map(k => this.parameters[k])
@@ -835,7 +929,7 @@ class Request extends BaseRequest {
835
929
  debug('request(%d): execute %s %O', IDS.get(this), procedure, logParams)
836
930
  }
837
931
 
838
- const req = new tds.Request(procedure, err => {
932
+ const req = new tds.Request(procedureName, err => {
839
933
  // to make sure we handle no-sql errors as well
840
934
  if (err && (!errors.length || (errors.length && err.message !== errors[errors.length - 1].message))) {
841
935
  err = new RequestError(err, 'EREQUEST')
@@ -1012,26 +1106,15 @@ class Request extends BaseRequest {
1012
1106
  }
1013
1107
  const param = this.parameters[name]
1014
1108
  if (param.io === 1) {
1015
- req.addParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: param.length, scale: param.scale, precision: param.precision })
1109
+ req.addParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: tediousLength(assertSafeTypeSize(param.length)), scale: assertSafeTypeSize(param.scale), precision: assertSafeTypeSize(param.precision) })
1016
1110
  } else {
1017
- req.addOutputParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: param.length, scale: param.scale, precision: param.precision })
1111
+ req.addOutputParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: tediousLength(assertSafeTypeSize(param.length)), scale: assertSafeTypeSize(param.scale), precision: assertSafeTypeSize(param.precision) })
1018
1112
  }
1019
1113
  }
1020
1114
 
1021
1115
  connection.callProcedure(req)
1022
1116
  } catch (error) {
1023
- const err = error instanceof RequestError ? error : new RequestError(error, 'EREQUEST')
1024
- delete this._cancel
1025
-
1026
- if (!hasReturned) {
1027
- for (const event in errorHandlers) {
1028
- connection.removeListener(event, errorHandlers[event])
1029
- }
1030
-
1031
- this.parent.release(connection)
1032
- hasReturned = true
1033
- callback(err)
1034
- }
1117
+ return abandon(error instanceof RequestError ? error : new RequestError(error, 'EREQUEST'))
1035
1118
  }
1036
1119
  })
1037
1120
  })
package/lib/utils.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const MSSQLError = require('./error/mssql-error')
4
+
3
5
  const IDS = new WeakMap()
4
6
  const INCREMENT = {
5
7
  Connection: 1,
@@ -17,8 +19,94 @@ const getPoolId = (obj) => {
17
19
  return parent ? IDS.get(parent) : undefined
18
20
  }
19
21
 
22
+ // Parameter names are emitted into generated T-SQL as `@<name>`. The `@` occupies the
23
+ // identifier's first position, so every character of the name sits in a subsequent
24
+ // position, where T-SQL also permits digits and `$`. The set is deliberately wider than
25
+ // the names the server will accept: it excludes the characters that could end the
26
+ // identifier and let the rest of the name be read as SQL, and nothing else, so a name
27
+ // the server dislikes still fails there rather than becoming an error here. The
28
+ // exception is the invisible format characters, which are rejected outright.
29
+ // Only the two zero-width joiners are taken from the format characters; the rest of that
30
+ // category includes ZERO WIDTH SPACE, which the server does treat as a separator.
31
+ const PARAM_NAME = /^(?:[\p{L}\p{N}\p{M}\p{Pc}@#$・・]|\u200C|\u200D)*$/u
32
+ // A table-valued parameter's type name is emitted into the parameter declaration, and a
33
+ // stored procedure's name is emitted into an `exec`, so both are checked the way the server
34
+ // reads a qualified name: one or more parts separated by `.`, each either bare, holding only
35
+ // the characters an identifier is made of, or quoted in brackets or double quotes, where
36
+ // anything goes until the closing mark. Nothing that gets through can end the statement it
37
+ // sits in and have the rest read as SQL.
38
+ const QUALIFIED_NAME_PART = '(?:\\[(?:[^\\]]|\\]\\])*\\]|"(?:[^"]|"")*"|[\\p{L}\\p{N}\\p{M}\\p{Pc}@#$]+)'
39
+ // An intermediate part may be omitted — `db..proc` and `server.db..proc` are ordinary
40
+ // T-SQL — but the first and last parts must be present, so a bare `.`, a leading `.proc`
41
+ // and a trailing `db..` are still refused. An empty part cannot end the identifier.
42
+ const QUALIFIED_NAME = new RegExp(`^(?:${QUALIFIED_NAME_PART}(?:\\.${QUALIFIED_NAME_PART}?)*\\.)?${QUALIFIED_NAME_PART}$`, 'u')
43
+
44
+ // A length, precision or scale is emitted into the same declaration. `max` is the only
45
+ // word the declaration takes there; everything else the server wants is a number.
46
+ const TYPE_SIZE = /^\w*$/
47
+
48
+ // A column name is emitted as a quoted identifier, `[name]`, by this module and again by
49
+ // the driver's own bulk load. Only `]` can terminate that quoting early, and doubling it
50
+ // is the documented way to include one literally.
51
+ const escapesQuotedIdentifier = (name) => String(name).replace(/]]/g, '').includes(']')
52
+
20
53
  module.exports = {
21
54
  objectHasProperty: (object, property) => Object.prototype.hasOwnProperty.call(object, property),
55
+ isValidParameterName: (name) => {
56
+ // Only primitives are accepted: an object could return a different value from a second
57
+ // toString() call, passing validation here and emitting something else into the SQL.
58
+ const type = typeof name
59
+ return (type === 'string' || type === 'number') && PARAM_NAME.test(String(name))
60
+ },
61
+ assertSafeTypeName: (name) => {
62
+ // A type name is optional: the tedious driver takes it from the Table passed as the
63
+ // value, and only uses this one when it is given.
64
+ if (name == null) {
65
+ return name
66
+ }
67
+ // Read once, and return what was read. Whatever is emitted is what was checked, so a
68
+ // value that reports a different name the second time cannot reach the SQL.
69
+ const type = String(name).trim()
70
+ if (!QUALIFIED_NAME.test(type)) {
71
+ throw new MSSQLError(`Invalid type name '${type}'. Type names may only contain identifier characters.`, 'EINJECT')
72
+ }
73
+ return type
74
+ },
75
+ assertSafeProcedureName: (name) => {
76
+ // The msnodesqlv8 driver builds `exec @___return___ = <name>` as SQL text, so the name is
77
+ // checked before it is emitted. The tedious driver sends it as a bound RPC and cannot be
78
+ // injected through it, but it is checked there too so that a name is accepted or rejected
79
+ // the same way whichever driver is in use.
80
+ // Read once, and return what was read, for the same reason as the type name.
81
+ const procedure = String(name).trim()
82
+ if (!QUALIFIED_NAME.test(procedure)) {
83
+ throw new MSSQLError(`Invalid procedure name '${procedure}'. Procedure names may only contain identifier characters.`, 'EINJECT')
84
+ }
85
+ return procedure
86
+ },
87
+ assertSafeTypeSize: (value) => {
88
+ if (value == null || typeof value === 'number') {
89
+ return value
90
+ }
91
+ // Read once and return what was read, for the same reason as the type name. Padding is
92
+ // trimmed rather than refused, and a word the server will reject still reaches it.
93
+ const size = String(value).trim()
94
+ if (!TYPE_SIZE.test(size)) {
95
+ throw new MSSQLError(`Invalid type size '${size}'. A length, precision or scale must be a number.`, 'EINJECT')
96
+ }
97
+ return size
98
+ },
99
+ assertSafeColumnName: (name) => {
100
+ const type = typeof name
101
+ if (type !== 'string' && type !== 'number') {
102
+ throw new MSSQLError(`Invalid column name '${String(name)}'. Column names must be a string or a number.`, 'EINJECT')
103
+ }
104
+ const column = String(name)
105
+ if (escapesQuotedIdentifier(column)) {
106
+ throw new MSSQLError(`Invalid column name '${column}'. A ']' in a column name must be written as ']]'.`, 'EINJECT')
107
+ }
108
+ return column
109
+ },
22
110
  INCREMENT,
23
111
  IDS: {
24
112
  get: IDS.get.bind(IDS),
package/package.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "azure",
22
22
  "node-mssql"
23
23
  ],
24
- "version": "12.7.1",
24
+ "version": "12.7.3",
25
25
  "main": "index.js",
26
26
  "type": "commonjs",
27
27
  "repository": {