@contrast/assess 1.6.0 → 1.7.0
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/lib/dataflow/propagation/index.js +1 -0
- package/lib/dataflow/propagation/install/contrast-methods/string.js +5 -1
- package/lib/dataflow/propagation/install/encode-uri-component.js +5 -2
- package/lib/dataflow/propagation/install/pug-runtime-escape.js +5 -2
- package/lib/dataflow/propagation/install/sequelize.js +310 -0
- package/lib/dataflow/propagation/install/sql-template-strings.js +5 -4
- package/lib/dataflow/propagation/install/string/match.js +2 -2
- package/lib/dataflow/propagation/install/string/replace.js +9 -4
- package/lib/dataflow/sinks/common.js +10 -1
- package/lib/dataflow/sinks/index.js +30 -1
- package/lib/dataflow/sinks/install/express/index.js +29 -0
- package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +134 -0
- package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +96 -69
- package/lib/dataflow/sinks/install/http.js +20 -5
- package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +33 -9
- package/lib/dataflow/sinks/install/mongodb.js +171 -54
- package/lib/dataflow/sinks/install/mssql.js +9 -4
- package/lib/dataflow/sinks/install/mysql.js +20 -4
- package/lib/dataflow/sinks/install/postgres.js +25 -12
- package/lib/dataflow/sinks/install/sequelize.js +142 -0
- package/lib/dataflow/sinks/install/sqlite3.js +9 -4
- package/lib/dataflow/sources/handler.js +4 -0
- package/lib/dataflow/sources/index.js +4 -1
- package/lib/dataflow/sources/install/body-parser1.js +120 -0
- package/lib/dataflow/sources/install/cookie-parser1.js +101 -0
- package/lib/dataflow/sources/install/express/index.js +28 -0
- package/package.json +3 -3
|
@@ -20,16 +20,18 @@ const {
|
|
|
20
20
|
DataflowTag: {
|
|
21
21
|
UNTRUSTED,
|
|
22
22
|
ALPHANUM_SPACE_HYPHEN,
|
|
23
|
+
CUSTOM_VALIDATED,
|
|
23
24
|
CUSTOM_VALIDATED_NOSQL_INJECTION,
|
|
24
25
|
LIMITED_CHARS,
|
|
25
26
|
STRING_TYPE_CHECKED,
|
|
26
27
|
},
|
|
27
|
-
Rule,
|
|
28
|
+
Rule: { NOSQL_INJECTION_MONGO },
|
|
28
29
|
isNonEmptyObject,
|
|
29
|
-
traverseValues
|
|
30
|
+
traverseValues,
|
|
31
|
+
isString
|
|
30
32
|
} = require('@contrast/common');
|
|
31
33
|
const utils = require('../../tag-utils');
|
|
32
|
-
const { patchType } = require('../common');
|
|
34
|
+
const { patchType, filterSafeTags } = require('../common');
|
|
33
35
|
|
|
34
36
|
const collectionMethods = [
|
|
35
37
|
'find',
|
|
@@ -46,9 +48,14 @@ const collectionMethods = [
|
|
|
46
48
|
'deleteOne',
|
|
47
49
|
'deleteMany',
|
|
48
50
|
];
|
|
51
|
+
const dbMethods = [
|
|
52
|
+
'command',
|
|
53
|
+
'eval'
|
|
54
|
+
];
|
|
49
55
|
|
|
50
56
|
const querySafeTags = [
|
|
51
57
|
ALPHANUM_SPACE_HYPHEN,
|
|
58
|
+
CUSTOM_VALIDATED,
|
|
52
59
|
CUSTOM_VALIDATED_NOSQL_INJECTION,
|
|
53
60
|
LIMITED_CHARS,
|
|
54
61
|
STRING_TYPE_CHECKED,
|
|
@@ -56,6 +63,7 @@ const querySafeTags = [
|
|
|
56
63
|
|
|
57
64
|
module.exports = function(core) {
|
|
58
65
|
const {
|
|
66
|
+
config,
|
|
59
67
|
depHooks,
|
|
60
68
|
logger,
|
|
61
69
|
patcher,
|
|
@@ -63,7 +71,7 @@ module.exports = function(core) {
|
|
|
63
71
|
assess: {
|
|
64
72
|
dataflow: {
|
|
65
73
|
tracker,
|
|
66
|
-
sinks: { isVulnerable, reportFindings },
|
|
74
|
+
sinks: { isVulnerable, runInActiveSink, isLocked, reportFindings, reportSafePositive },
|
|
67
75
|
eventFactory: { createSinkEvent }
|
|
68
76
|
}
|
|
69
77
|
}
|
|
@@ -74,18 +82,36 @@ module.exports = function(core) {
|
|
|
74
82
|
|
|
75
83
|
instr.getVulnerabilityInfo = function getVulnerabilityInfo(query) {
|
|
76
84
|
let vulnInfo = null;
|
|
85
|
+
let reportSafe = null;
|
|
86
|
+
|
|
87
|
+
if (isString(query)) {
|
|
88
|
+
const strInfo = tracker.getData(query);
|
|
89
|
+
if (strInfo) {
|
|
90
|
+
if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
|
|
91
|
+
vulnInfo = { strInfo };
|
|
92
|
+
} else {
|
|
93
|
+
reportSafe = { strInfo };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
77
96
|
|
|
78
|
-
|
|
97
|
+
return { vulnInfo, reportSafe };
|
|
98
|
+
}
|
|
79
99
|
|
|
80
|
-
|
|
100
|
+
if (!isNonEmptyObject(query)) return { vulnInfo, reportSafe };
|
|
101
|
+
|
|
102
|
+
traverseValues(query, (path, _type, value) => {
|
|
81
103
|
const strInfo = tracker.getData(value);
|
|
82
|
-
if (strInfo
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
if (strInfo) {
|
|
105
|
+
if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
|
|
106
|
+
vulnInfo = { path, strInfo };
|
|
107
|
+
return true; // halts traversal
|
|
108
|
+
} else {
|
|
109
|
+
reportSafe = { path, strInfo };
|
|
110
|
+
}
|
|
85
111
|
}
|
|
86
112
|
});
|
|
87
113
|
|
|
88
|
-
return vulnInfo;
|
|
114
|
+
return { vulnInfo, reportSafe };
|
|
89
115
|
};
|
|
90
116
|
|
|
91
117
|
instr.install = function() {
|
|
@@ -99,12 +125,10 @@ module.exports = function(core) {
|
|
|
99
125
|
|
|
100
126
|
function patchCollection(mongodb, version) {
|
|
101
127
|
for (const method of collectionMethods) {
|
|
102
|
-
|
|
103
128
|
const proto = mongodb.Collection.prototype;
|
|
104
129
|
const name = `mongodb.Collection.prototype.${method}`;
|
|
105
130
|
|
|
106
131
|
if (!proto[method]) {
|
|
107
|
-
|
|
108
132
|
logger.trace({ name, version }, 'method not found - skipping instrumentation');
|
|
109
133
|
continue;
|
|
110
134
|
}
|
|
@@ -113,49 +137,61 @@ module.exports = function(core) {
|
|
|
113
137
|
name,
|
|
114
138
|
patchType,
|
|
115
139
|
around(next, data) {
|
|
116
|
-
const { obj, args } = data;
|
|
140
|
+
const { obj, args: origArgs } = data;
|
|
117
141
|
const sourceCtx = sources.getStore()?.assess;
|
|
118
142
|
|
|
119
|
-
if (instrumentation.isLocked() || !sourceCtx) {
|
|
143
|
+
if (isLocked(NOSQL_INJECTION_MONGO) || instrumentation.isLocked() || !sourceCtx) {
|
|
120
144
|
return next();
|
|
121
145
|
}
|
|
122
146
|
|
|
123
147
|
const argIdx = 0;
|
|
124
148
|
try {
|
|
125
|
-
const vulnInfo = instr.getVulnerabilityInfo(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const args = data.args.map((arg, idx) => ({
|
|
130
|
-
value: inspect(arg),
|
|
131
|
-
tracked: idx === argIdx,
|
|
132
|
-
}));
|
|
133
|
-
|
|
134
|
-
const tags = getAdjustedQueryTags(path, strInfo, args[argIdx].value);
|
|
135
|
-
const resultVal = args[args.length - 1].value.startsWith('[Function') ? '' : 'Promise';
|
|
136
|
-
const sinkEvent = createSinkEvent({
|
|
137
|
-
args,
|
|
138
|
-
context: `${objName}.${method}(${args.map((a) => a.value)})`,
|
|
139
|
-
history: [strInfo],
|
|
140
|
-
object: {
|
|
141
|
-
tracked: false,
|
|
142
|
-
value: 'mongodb.Collection',
|
|
143
|
-
},
|
|
149
|
+
const { vulnInfo, reportSafe } = instr.getVulnerabilityInfo(origArgs[argIdx]);
|
|
150
|
+
|
|
151
|
+
if (!vulnInfo) {
|
|
152
|
+
reportSafe && config.assess.safe_positives.enable && reportSafePositive({
|
|
144
153
|
name,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
ruleId: NOSQL_INJECTION_MONGO,
|
|
155
|
+
safeTags: filterSafeTags(querySafeTags, reportSafe.strInfo),
|
|
156
|
+
strInfo: {
|
|
157
|
+
value: inspect(origArgs[argIdx]),
|
|
158
|
+
tags: getAdjustedQueryTags(reportSafe.path, reportSafe.strInfo, inspect(origArgs[argIdx])),
|
|
148
159
|
},
|
|
149
|
-
source: `P${argIdx}`,
|
|
150
|
-
stacktraceOpts: {
|
|
151
|
-
constructorOpt: data.hooked,
|
|
152
|
-
},
|
|
153
|
-
tags,
|
|
154
160
|
});
|
|
161
|
+
return method === 'findOne' ? runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next()) : next();
|
|
162
|
+
}
|
|
155
163
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
const { path, strInfo } = vulnInfo;
|
|
165
|
+
const objName = getObjectName(obj);
|
|
166
|
+
const args = origArgs.map((arg, idx) => ({
|
|
167
|
+
value: inspect(arg),
|
|
168
|
+
tracked: idx === argIdx,
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
const tags = getAdjustedQueryTags(path, strInfo, args[argIdx].value);
|
|
172
|
+
const resultVal = args[args.length - 1].value.startsWith('[Function') ? '' : 'Promise';
|
|
173
|
+
const sinkEvent = createSinkEvent({
|
|
174
|
+
args,
|
|
175
|
+
context: `${objName}.${method}(${args.map((a) => a.value)})`,
|
|
176
|
+
history: [strInfo],
|
|
177
|
+
object: {
|
|
178
|
+
tracked: false,
|
|
179
|
+
value: 'mongodb.Collection',
|
|
180
|
+
},
|
|
181
|
+
name,
|
|
182
|
+
result: {
|
|
183
|
+
tracked: false,
|
|
184
|
+
value: resultVal,
|
|
185
|
+
},
|
|
186
|
+
source: `P${argIdx}`,
|
|
187
|
+
stacktraceOpts: {
|
|
188
|
+
constructorOpt: data.hooked,
|
|
189
|
+
},
|
|
190
|
+
tags,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (sinkEvent) {
|
|
194
|
+
reportFindings({ ruleId: NOSQL_INJECTION_MONGO, sinkEvent });
|
|
159
195
|
}
|
|
160
196
|
} catch (err) {
|
|
161
197
|
core.logger.error({ name, err }, 'assess sink analysis failed');
|
|
@@ -163,11 +199,7 @@ module.exports = function(core) {
|
|
|
163
199
|
|
|
164
200
|
if (method === 'findOne') {
|
|
165
201
|
// `findOne` will call `find` so don't analyze in nested call
|
|
166
|
-
|
|
167
|
-
const ret = instrumentation.run(store, next);
|
|
168
|
-
// but unlock for when callback args run or returned Promises resolve/reject
|
|
169
|
-
store.lock = false;
|
|
170
|
-
return ret;
|
|
202
|
+
return runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next());
|
|
171
203
|
}
|
|
172
204
|
|
|
173
205
|
return next();
|
|
@@ -176,16 +208,101 @@ module.exports = function(core) {
|
|
|
176
208
|
}
|
|
177
209
|
}
|
|
178
210
|
|
|
179
|
-
|
|
180
211
|
function patchDatabase(mongodb, version) {
|
|
181
|
-
|
|
212
|
+
for (const method of dbMethods) {
|
|
213
|
+
const proto = mongodb.Db.prototype;
|
|
214
|
+
const name = `mongodb.Db.prototype.${method}`;
|
|
215
|
+
|
|
216
|
+
if (!proto[method]) {
|
|
217
|
+
logger.trace({ name, version }, 'method not found - skipping instrumentation');
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
patcher.patch(proto, method, {
|
|
222
|
+
name,
|
|
223
|
+
patchType,
|
|
224
|
+
around(next, data) {
|
|
225
|
+
const { obj, args: origArgs } = data;
|
|
226
|
+
const sourceCtx = sources.getStore()?.assess;
|
|
227
|
+
|
|
228
|
+
if (isLocked(NOSQL_INJECTION_MONGO) || instrumentation.isLocked() || !sourceCtx) {
|
|
229
|
+
return next();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const argIdx = 0;
|
|
233
|
+
try {
|
|
234
|
+
const { vulnInfo, reportSafe } = instr.getVulnerabilityInfo(origArgs[argIdx]);
|
|
235
|
+
|
|
236
|
+
if (!vulnInfo) {
|
|
237
|
+
const tags = reportSafe?.path ? getAdjustedQueryTags(reportSafe.path, reportSafe.strInfo, inspect(origArgs[argIdx])) : reportSafe?.strInfo?.tags;
|
|
238
|
+
reportSafe && config.assess.safe_positives.enable && reportSafePositive({
|
|
239
|
+
name,
|
|
240
|
+
ruleId: NOSQL_INJECTION_MONGO,
|
|
241
|
+
safeTags: filterSafeTags(querySafeTags, reportSafe.strInfo),
|
|
242
|
+
strInfo: {
|
|
243
|
+
value: inspect(origArgs[argIdx]),
|
|
244
|
+
tags
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
return method === 'eval' ? runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next()) : next();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const { path, strInfo } = vulnInfo;
|
|
251
|
+
const objName = getObjectName(obj, 'Db');
|
|
252
|
+
const args = origArgs.map((arg, idx) => ({
|
|
253
|
+
value: isString(arg) ? arg : inspect(arg),
|
|
254
|
+
tracked: idx === argIdx,
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
const tags = path ? getAdjustedQueryTags(path, strInfo, args[argIdx].value) : strInfo?.tags;
|
|
258
|
+
const resultVal = args[args.length - 1].value.startsWith('[Function') ? '' : 'Promise';
|
|
259
|
+
const sinkEvent = createSinkEvent({
|
|
260
|
+
args,
|
|
261
|
+
context: `${objName}.${method}(${args.map((a) => a.value)})`,
|
|
262
|
+
history: [strInfo],
|
|
263
|
+
object: {
|
|
264
|
+
tracked: false,
|
|
265
|
+
value: 'mongodb.Db',
|
|
266
|
+
},
|
|
267
|
+
name,
|
|
268
|
+
result: {
|
|
269
|
+
tracked: false,
|
|
270
|
+
value: resultVal,
|
|
271
|
+
},
|
|
272
|
+
source: `P${argIdx}`,
|
|
273
|
+
stacktraceOpts: {
|
|
274
|
+
constructorOpt: data.hooked,
|
|
275
|
+
},
|
|
276
|
+
tags,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (sinkEvent) {
|
|
280
|
+
reportFindings({ ruleId: NOSQL_INJECTION_MONGO, sinkEvent });
|
|
281
|
+
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
core.logger.error({ name, err }, 'assess sink analysis failed');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (method === 'eval') {
|
|
287
|
+
// `eval` will call `command` so don't analyze in nested call
|
|
288
|
+
return runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next());
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return next();
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
182
295
|
}
|
|
183
296
|
|
|
184
|
-
function getObjectName(obj) {
|
|
297
|
+
function getObjectName(obj, entity) {
|
|
185
298
|
let name = '';
|
|
186
299
|
name += obj.s?.namespace?.db || 'db';
|
|
187
|
-
|
|
188
|
-
|
|
300
|
+
|
|
301
|
+
if (entity !== 'Db') {
|
|
302
|
+
name += '.';
|
|
303
|
+
name += obj.s?.namespace?.collection || 'collection';
|
|
304
|
+
}
|
|
305
|
+
|
|
189
306
|
return name;
|
|
190
307
|
}
|
|
191
308
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
const {
|
|
19
19
|
DataflowTag: { UNTRUSTED, SQL_ENCODED, LIMITED_CHARS, CUSTOM_VALIDATED, CUSTOM_ENCODED },
|
|
20
|
-
Rule,
|
|
20
|
+
Rule: { SQL_INJECTION },
|
|
21
21
|
isString
|
|
22
22
|
} = require('@contrast/common');
|
|
23
23
|
const { createModuleLabel } = require('../../propagation/common');
|
|
@@ -38,7 +38,7 @@ module.exports = function (core) {
|
|
|
38
38
|
assess: {
|
|
39
39
|
dataflow: {
|
|
40
40
|
tracker,
|
|
41
|
-
sinks: { isVulnerable, reportFindings },
|
|
41
|
+
sinks: { isVulnerable, isLocked, reportFindings },
|
|
42
42
|
eventFactory: { createSinkEvent },
|
|
43
43
|
},
|
|
44
44
|
},
|
|
@@ -46,7 +46,12 @@ module.exports = function (core) {
|
|
|
46
46
|
|
|
47
47
|
const pre = (name, obj, version) => (data) => {
|
|
48
48
|
const store = sources.getStore()?.assess;
|
|
49
|
-
if (
|
|
49
|
+
if (
|
|
50
|
+
!store ||
|
|
51
|
+
!data.args[0] ||
|
|
52
|
+
!isString(data.args[0]) ||
|
|
53
|
+
isLocked(SQL_INJECTION)
|
|
54
|
+
) return;
|
|
50
55
|
|
|
51
56
|
const strInfo = tracker.getData(data.args[0]);
|
|
52
57
|
if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
|
|
@@ -75,7 +80,7 @@ module.exports = function (core) {
|
|
|
75
80
|
|
|
76
81
|
if (event) {
|
|
77
82
|
reportFindings({
|
|
78
|
-
ruleId:
|
|
83
|
+
ruleId: SQL_INJECTION,
|
|
79
84
|
sinkEvent: event,
|
|
80
85
|
});
|
|
81
86
|
}
|
|
@@ -16,7 +16,19 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const { patchType } = require('../common');
|
|
19
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
Rule: { SQL_INJECTION },
|
|
21
|
+
isString,
|
|
22
|
+
DataflowTag: {
|
|
23
|
+
CUSTOM_ENCODED_SQL_INJECTION,
|
|
24
|
+
CUSTOM_ENCODED,
|
|
25
|
+
CUSTOM_VALIDATED_SQL_INJECTION,
|
|
26
|
+
CUSTOM_VALIDATED,
|
|
27
|
+
SQL_ENCODED,
|
|
28
|
+
LIMITED_CHARS,
|
|
29
|
+
UNTRUSTED
|
|
30
|
+
},
|
|
31
|
+
} = require('@contrast/common');
|
|
20
32
|
|
|
21
33
|
const safeTags = [
|
|
22
34
|
CUSTOM_ENCODED_SQL_INJECTION,
|
|
@@ -35,7 +47,7 @@ module.exports = function (core) {
|
|
|
35
47
|
assess: {
|
|
36
48
|
dataflow: {
|
|
37
49
|
tracker,
|
|
38
|
-
sinks: { isVulnerable, reportFindings },
|
|
50
|
+
sinks: { isVulnerable, isLocked, reportFindings },
|
|
39
51
|
eventFactory: { createSinkEvent },
|
|
40
52
|
},
|
|
41
53
|
},
|
|
@@ -53,7 +65,11 @@ module.exports = function (core) {
|
|
|
53
65
|
|
|
54
66
|
const pre = (module, file, obj) => (data) => {
|
|
55
67
|
const store = sources.getStore()?.assess;
|
|
56
|
-
if (
|
|
68
|
+
if (
|
|
69
|
+
!store ||
|
|
70
|
+
!data.args[0] ||
|
|
71
|
+
isLocked(SQL_INJECTION)
|
|
72
|
+
) return;
|
|
57
73
|
|
|
58
74
|
const val = getValueFromArgs(data.args);
|
|
59
75
|
if (!val) return;
|
|
@@ -85,7 +101,7 @@ module.exports = function (core) {
|
|
|
85
101
|
|
|
86
102
|
if (event) {
|
|
87
103
|
reportFindings({
|
|
88
|
-
ruleId:
|
|
104
|
+
ruleId: SQL_INJECTION,
|
|
89
105
|
sinkEvent: event,
|
|
90
106
|
});
|
|
91
107
|
}
|
|
@@ -18,20 +18,21 @@
|
|
|
18
18
|
const util = require('util');
|
|
19
19
|
const {
|
|
20
20
|
DataflowTag: { UNTRUSTED, SQL_ENCODED, LIMITED_CHARS, CUSTOM_VALIDATED, CUSTOM_ENCODED },
|
|
21
|
-
Rule,
|
|
21
|
+
Rule: { SQL_INJECTION: ruleId },
|
|
22
22
|
isString
|
|
23
23
|
} = require('@contrast/common');
|
|
24
|
-
const { patchType } = require('../common');
|
|
24
|
+
const { filterSafeTags, patchType } = require('../common');
|
|
25
25
|
|
|
26
26
|
module.exports = function (core) {
|
|
27
27
|
const {
|
|
28
|
+
config,
|
|
28
29
|
depHooks,
|
|
29
30
|
patcher,
|
|
30
31
|
scopes: { sources },
|
|
31
32
|
assess: {
|
|
32
33
|
dataflow: {
|
|
33
34
|
tracker,
|
|
34
|
-
sinks: { isVulnerable, reportFindings },
|
|
35
|
+
sinks: { isVulnerable, isLocked, reportFindings, reportSafePositive },
|
|
35
36
|
eventFactory: { createSinkEvent },
|
|
36
37
|
},
|
|
37
38
|
},
|
|
@@ -48,15 +49,17 @@ module.exports = function (core) {
|
|
|
48
49
|
|
|
49
50
|
const postgres = core.assess.dataflow.sinks.postgres = {};
|
|
50
51
|
|
|
51
|
-
const preHook = (methodSignature
|
|
52
|
+
const preHook = (methodSignature) => (data) => {
|
|
52
53
|
const assessStore = sources.getStore()?.assess;
|
|
53
|
-
if (!assessStore) return;
|
|
54
|
+
if (!assessStore || isLocked(ruleId)) return;
|
|
54
55
|
|
|
55
56
|
const [arg0] = data.args;
|
|
56
57
|
const query = arg0?.text || arg0;
|
|
57
58
|
if (!query || !isString(query)) return;
|
|
58
59
|
|
|
59
60
|
const strInfo = tracker.getData(query);
|
|
61
|
+
// todo: if the query isn't tracked but users are sending tracked strings in the values
|
|
62
|
+
// array (args[1]), then shouldn't we report a "safe-positive" event of some kind?
|
|
60
63
|
if (!strInfo) return;
|
|
61
64
|
|
|
62
65
|
const objValue = methodSignature.includes('native') ? 'pg.native.Client' : 'pg.Client';
|
|
@@ -88,10 +91,20 @@ module.exports = function (core) {
|
|
|
88
91
|
|
|
89
92
|
if (event) {
|
|
90
93
|
reportFindings({
|
|
91
|
-
ruleId
|
|
94
|
+
ruleId,
|
|
92
95
|
sinkEvent: event,
|
|
93
96
|
});
|
|
94
97
|
}
|
|
98
|
+
} else if (config.assess.safe_positives.enable) {
|
|
99
|
+
reportSafePositive({
|
|
100
|
+
name: methodSignature,
|
|
101
|
+
ruleId,
|
|
102
|
+
safeTags: filterSafeTags(safeTags, strInfo),
|
|
103
|
+
strInfo: {
|
|
104
|
+
value: strInfo.value,
|
|
105
|
+
tags: strInfo.tags,
|
|
106
|
+
}
|
|
107
|
+
});
|
|
95
108
|
}
|
|
96
109
|
};
|
|
97
110
|
|
|
@@ -99,11 +112,11 @@ module.exports = function (core) {
|
|
|
99
112
|
const pgClientQueryPatchName = 'pg.Client.prototype.query';
|
|
100
113
|
depHooks.resolve(
|
|
101
114
|
{ name: 'pg', file: 'lib/client.js' },
|
|
102
|
-
(client
|
|
115
|
+
(client) => {
|
|
103
116
|
patcher.patch(client.prototype, 'query', {
|
|
104
117
|
name: pgClientQueryPatchName,
|
|
105
118
|
patchType,
|
|
106
|
-
pre: preHook('pg/lib/client.prototype.query'
|
|
119
|
+
pre: preHook('pg/lib/client.prototype.query'),
|
|
107
120
|
});
|
|
108
121
|
},
|
|
109
122
|
);
|
|
@@ -111,16 +124,16 @@ module.exports = function (core) {
|
|
|
111
124
|
const pgNativeClientQueryPatchName = 'pg.native.Client.prototype.query';
|
|
112
125
|
depHooks.resolve(
|
|
113
126
|
{ name: 'pg', file: 'lib/native/client.js' },
|
|
114
|
-
(client
|
|
127
|
+
(client) => {
|
|
115
128
|
patcher.patch(client.prototype, 'query', {
|
|
116
129
|
name: pgNativeClientQueryPatchName,
|
|
117
130
|
patchType,
|
|
118
|
-
pre: preHook('pg/lib/native/client.prototype.query'
|
|
131
|
+
pre: preHook('pg/lib/native/client.prototype.query'),
|
|
119
132
|
});
|
|
120
133
|
},
|
|
121
134
|
);
|
|
122
135
|
|
|
123
|
-
depHooks.resolve({ name: 'pg-pool' }, (pool
|
|
136
|
+
depHooks.resolve({ name: 'pg-pool' }, (pool) => {
|
|
124
137
|
const name = 'pg-pool.Pool.prototype.query';
|
|
125
138
|
patcher.patch(pool.prototype, 'query', {
|
|
126
139
|
name,
|
|
@@ -137,7 +150,7 @@ module.exports = function (core) {
|
|
|
137
150
|
return;
|
|
138
151
|
}
|
|
139
152
|
|
|
140
|
-
preHook(name
|
|
153
|
+
preHook(name)(data);
|
|
141
154
|
},
|
|
142
155
|
});
|
|
143
156
|
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2022 Contrast Security, Inc
|
|
3
|
+
* Contact: support@contrastsecurity.com
|
|
4
|
+
* License: Commercial
|
|
5
|
+
|
|
6
|
+
* NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
* used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
* made available through public repositories, use of this Software is subject to
|
|
9
|
+
* the applicable End User Licensing Agreement found at
|
|
10
|
+
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
* between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
* way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const util = require('util');
|
|
19
|
+
const {
|
|
20
|
+
Rule: { SQL_INJECTION },
|
|
21
|
+
DataflowTag: {
|
|
22
|
+
UNTRUSTED,
|
|
23
|
+
SQL_ENCODED,
|
|
24
|
+
LIMITED_CHARS,
|
|
25
|
+
CUSTOM_VALIDATED,
|
|
26
|
+
CUSTOM_ENCODED,
|
|
27
|
+
},
|
|
28
|
+
} = require('@contrast/common');
|
|
29
|
+
const { patchType, filterSafeTags } = require('../common');
|
|
30
|
+
|
|
31
|
+
module.exports = function (core) {
|
|
32
|
+
const {
|
|
33
|
+
depHooks,
|
|
34
|
+
patcher,
|
|
35
|
+
config,
|
|
36
|
+
scopes: { sources },
|
|
37
|
+
assess: {
|
|
38
|
+
dataflow: {
|
|
39
|
+
tracker,
|
|
40
|
+
sinks: { isVulnerable, runInActiveSink, reportFindings, reportSafePositive },
|
|
41
|
+
eventFactory: { createSinkEvent },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
} = core;
|
|
45
|
+
|
|
46
|
+
const safeTags = [
|
|
47
|
+
SQL_ENCODED,
|
|
48
|
+
LIMITED_CHARS,
|
|
49
|
+
CUSTOM_VALIDATED,
|
|
50
|
+
CUSTOM_ENCODED
|
|
51
|
+
];
|
|
52
|
+
const requiredTag = UNTRUSTED;
|
|
53
|
+
const inspect = patcher.unwrap(util.inspect);
|
|
54
|
+
|
|
55
|
+
const sequelize = (core.assess.dataflow.sinks.sequelize = {});
|
|
56
|
+
|
|
57
|
+
sequelize.install = function () {
|
|
58
|
+
const sequelizeQueryPatchName = 'sequelize.prototype.query';
|
|
59
|
+
depHooks.resolve({ name: 'sequelize' }, (sequelize) => {
|
|
60
|
+
patcher.patch(sequelize.prototype, 'query', {
|
|
61
|
+
name: sequelizeQueryPatchName,
|
|
62
|
+
patchType,
|
|
63
|
+
around(next, data) {
|
|
64
|
+
const { args, hooked, orig } = data;
|
|
65
|
+
const sourceContext = sources.getStore()?.assess;
|
|
66
|
+
if (!sourceContext || !args[0]) return next();
|
|
67
|
+
|
|
68
|
+
const query = typeof args[0] === 'string' ? args[0] : args[0].query;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const queryInfo = tracker.getData(query);
|
|
72
|
+
const isVulnerableQuery = isVulnerable(requiredTag, safeTags, queryInfo.tags);
|
|
73
|
+
|
|
74
|
+
if (queryInfo && !isVulnerableQuery && config.assess.safe_positives.enable) {
|
|
75
|
+
reportSafePositive({
|
|
76
|
+
name: sequelizeQueryPatchName,
|
|
77
|
+
ruleId: SQL_INJECTION,
|
|
78
|
+
safeTags: filterSafeTags(safeTags, queryInfo),
|
|
79
|
+
strInfo: {
|
|
80
|
+
value: queryInfo?.value,
|
|
81
|
+
tags: queryInfo.tags,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
!queryInfo ||
|
|
88
|
+
!isVulnerableQuery
|
|
89
|
+
) {
|
|
90
|
+
return runInActiveSink(SQL_INJECTION, async () => await next());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const sqlValue =
|
|
94
|
+
typeof args[0] === 'string' ? args[0] : inspect(args[0]);
|
|
95
|
+
const inspectedOptions = args[1] ? inspect(args[1]) : '';
|
|
96
|
+
const contextArgs = args[1]
|
|
97
|
+
? `${sqlValue}, ${inspectedOptions}`
|
|
98
|
+
: sqlValue;
|
|
99
|
+
|
|
100
|
+
const reportedArgs = [{ value: sqlValue, tracked: true }];
|
|
101
|
+
args[1] &&
|
|
102
|
+
reportedArgs.push({ value: inspectedOptions, tracked: false });
|
|
103
|
+
|
|
104
|
+
const event = createSinkEvent({
|
|
105
|
+
context: `sequelize.prototype.query(${contextArgs})`,
|
|
106
|
+
name: sequelizeQueryPatchName,
|
|
107
|
+
history: [queryInfo],
|
|
108
|
+
object: {
|
|
109
|
+
value: 'sequelize.prototype',
|
|
110
|
+
tracked: false,
|
|
111
|
+
},
|
|
112
|
+
args: reportedArgs,
|
|
113
|
+
tags: queryInfo?.tags,
|
|
114
|
+
source: 'P0',
|
|
115
|
+
stacktraceOpts: {
|
|
116
|
+
contructorOpt: hooked,
|
|
117
|
+
prependFrames: [orig],
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (event) {
|
|
122
|
+
reportFindings({
|
|
123
|
+
ruleId: SQL_INJECTION,
|
|
124
|
+
sinkEvent: event,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/* c8 ignore next 3 */
|
|
128
|
+
} catch (err) {
|
|
129
|
+
core.logger.error(
|
|
130
|
+
{ name: sequelizeQueryPatchName, err },
|
|
131
|
+
'assess sink analysis failed'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return runInActiveSink(SQL_INJECTION, async () => await next());
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return sequelize;
|
|
142
|
+
};
|