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 +111 -6
- package/lib/base/prepared-statement.js +18 -10
- package/lib/base/request.js +5 -5
- package/lib/datatypes.js +26 -13
- package/lib/msnodesqlv8/request.js +120 -37
- package/lib/table.js +3 -3
- package/lib/tedious/request.js +125 -42
- package/lib/utils.js +88 -0
- package/package.json +1 -1
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`) -
|
|
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`) -
|
|
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`) -
|
|
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`) -
|
|
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 |
|
|
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 |
|
|
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 (
|
|
101
|
-
throw new PreparedStatementError(`
|
|
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 (
|
|
154
|
-
throw new PreparedStatementError(`
|
|
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
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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) {
|
package/lib/base/request.js
CHANGED
|
@@ -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 (
|
|
119
|
-
throw new RequestError(`
|
|
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 (
|
|
182
|
-
throw new RequestError(`
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
case TYPES.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
case TYPES.
|
|
146
|
-
|
|
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
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
382
|
-
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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___ = ${
|
|
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')
|
|
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')
|
package/lib/tedious/request.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
691
|
-
|
|
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(
|
|
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
|
-
|
|
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),
|