mssql 9.1.2 → 9.1.4

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.
@@ -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.1.2",
24
+ "version": "9.1.4",
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": {