@contrast/assess 1.6.0 → 1.8.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 +3 -0
- package/lib/dataflow/propagation/install/JSON/index.js +33 -0
- package/lib/dataflow/propagation/install/JSON/stringify.js +290 -0
- package/lib/dataflow/propagation/install/buffer.js +79 -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 +297 -82
- 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 +144 -26
- package/lib/dataflow/sources/index.js +6 -8
- package/lib/dataflow/sources/install/body-parser1.js +133 -0
- package/lib/dataflow/sources/install/cookie-parser1.js +101 -0
- package/lib/dataflow/sources/install/express/index.js +31 -0
- package/lib/dataflow/sources/install/express/params.js +81 -0
- package/lib/dataflow/sources/install/express/parsedUrl.js +87 -0
- package/lib/dataflow/sources/install/http.js +32 -18
- package/lib/dataflow/sources/install/querystring.js +75 -0
- package/lib/dataflow/tag-utils.js +68 -1
- 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',
|
|
@@ -45,10 +47,20 @@ const collectionMethods = [
|
|
|
45
47
|
'updateMany',
|
|
46
48
|
'deleteOne',
|
|
47
49
|
'deleteMany',
|
|
50
|
+
'aggregate',
|
|
51
|
+
'mapReduce',
|
|
52
|
+
'group'
|
|
48
53
|
];
|
|
54
|
+
const dbMethods = [
|
|
55
|
+
'command',
|
|
56
|
+
'eval',
|
|
57
|
+
'aggregate'
|
|
58
|
+
];
|
|
59
|
+
const methodsWithNestedCalls = ['findOne', 'eval', 'group'];
|
|
49
60
|
|
|
50
61
|
const querySafeTags = [
|
|
51
62
|
ALPHANUM_SPACE_HYPHEN,
|
|
63
|
+
CUSTOM_VALIDATED,
|
|
52
64
|
CUSTOM_VALIDATED_NOSQL_INJECTION,
|
|
53
65
|
LIMITED_CHARS,
|
|
54
66
|
STRING_TYPE_CHECKED,
|
|
@@ -56,6 +68,7 @@ const querySafeTags = [
|
|
|
56
68
|
|
|
57
69
|
module.exports = function(core) {
|
|
58
70
|
const {
|
|
71
|
+
config,
|
|
59
72
|
depHooks,
|
|
60
73
|
logger,
|
|
61
74
|
patcher,
|
|
@@ -63,7 +76,7 @@ module.exports = function(core) {
|
|
|
63
76
|
assess: {
|
|
64
77
|
dataflow: {
|
|
65
78
|
tracker,
|
|
66
|
-
sinks: { isVulnerable, reportFindings },
|
|
79
|
+
sinks: { isVulnerable, runInActiveSink, isLocked, reportFindings, reportSafePositive },
|
|
67
80
|
eventFactory: { createSinkEvent }
|
|
68
81
|
}
|
|
69
82
|
}
|
|
@@ -72,22 +85,236 @@ module.exports = function(core) {
|
|
|
72
85
|
const inspect = patcher.unwrap(util.inspect);
|
|
73
86
|
const instr = core.assess.dataflow.sinks.mongodb = {};
|
|
74
87
|
|
|
75
|
-
instr.
|
|
88
|
+
instr.getQueryVulnerabilityInfo = function getQueryVulnerabilityInfo(query) {
|
|
76
89
|
let vulnInfo = null;
|
|
90
|
+
let reportSafe = null;
|
|
91
|
+
|
|
92
|
+
if (isString(query)) {
|
|
93
|
+
const strInfo = tracker.getData(query);
|
|
94
|
+
if (strInfo) {
|
|
95
|
+
if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
|
|
96
|
+
vulnInfo = { strInfo };
|
|
97
|
+
} else {
|
|
98
|
+
reportSafe = { strInfo };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
77
101
|
|
|
78
|
-
|
|
102
|
+
return { vulnInfo, reportSafe };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!isNonEmptyObject(query)) return { vulnInfo, reportSafe };
|
|
79
106
|
|
|
80
|
-
traverseValues(query, (path,
|
|
107
|
+
traverseValues(query, (path, _type, value) => {
|
|
81
108
|
const strInfo = tracker.getData(value);
|
|
82
|
-
if (strInfo
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
if (strInfo) {
|
|
110
|
+
if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
|
|
111
|
+
vulnInfo = { path: [...path], strInfo };
|
|
112
|
+
return true; // halts traversal
|
|
113
|
+
} else {
|
|
114
|
+
reportSafe = { path: [...path], strInfo };
|
|
115
|
+
}
|
|
85
116
|
}
|
|
86
117
|
});
|
|
87
118
|
|
|
88
|
-
return vulnInfo;
|
|
119
|
+
return { vulnInfo, reportSafe };
|
|
89
120
|
};
|
|
90
121
|
|
|
122
|
+
instr.getAggregateVulnerabilityInfo = function getAggregateVulnerabilityInfo(aggregation) {
|
|
123
|
+
let vulnInfo = null;
|
|
124
|
+
let reportSafe = null;
|
|
125
|
+
|
|
126
|
+
if (!isNonEmptyObject(aggregation)) return { vulnInfo, reportSafe };
|
|
127
|
+
|
|
128
|
+
traverseValues(aggregation, (path, _type, value) => {
|
|
129
|
+
const accumulatorFuncProps = [
|
|
130
|
+
'init',
|
|
131
|
+
'merge',
|
|
132
|
+
'accumulate',
|
|
133
|
+
'finalize'
|
|
134
|
+
];
|
|
135
|
+
const lastIdx = path.length - 1;
|
|
136
|
+
if (
|
|
137
|
+
(path[lastIdx - 1] === '$function' && path[lastIdx] === 'body') ||
|
|
138
|
+
(path[lastIdx - 1] === '$accumulator' && accumulatorFuncProps.includes(path[lastIdx]))
|
|
139
|
+
) {
|
|
140
|
+
const strInfo = tracker.getData(value);
|
|
141
|
+
if (strInfo) {
|
|
142
|
+
if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
|
|
143
|
+
vulnInfo = { path: [...path], strInfo };
|
|
144
|
+
return true; // halts traversal
|
|
145
|
+
} else {
|
|
146
|
+
reportSafe = { path: [...path], strInfo };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return { vulnInfo, reportSafe };
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
instr.getMapReduceVulnerabilityInfo = function getMapReduceVulnerabilityInfo(argToCheck, argIdx) {
|
|
156
|
+
let vulnInfo = null;
|
|
157
|
+
let reportSafe = null;
|
|
158
|
+
|
|
159
|
+
if (argIdx !== 2 && isString(argToCheck)) {
|
|
160
|
+
const strInfo = tracker.getData(argToCheck);
|
|
161
|
+
if (strInfo) {
|
|
162
|
+
if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
|
|
163
|
+
vulnInfo = { strInfo };
|
|
164
|
+
} else {
|
|
165
|
+
reportSafe = { strInfo };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { vulnInfo, reportSafe };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!isNonEmptyObject(argToCheck)) return { vulnInfo, reportSafe };
|
|
173
|
+
|
|
174
|
+
traverseValues(argToCheck, (path, _type, value) => {
|
|
175
|
+
const vulnerableProps = [
|
|
176
|
+
'query',
|
|
177
|
+
'finalize',
|
|
178
|
+
];
|
|
179
|
+
if (vulnerableProps.includes(path[0])) {
|
|
180
|
+
const strInfo = tracker.getData(value);
|
|
181
|
+
if (strInfo) {
|
|
182
|
+
if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
|
|
183
|
+
vulnInfo = { path: [...path], strInfo };
|
|
184
|
+
return true; // halts traversal
|
|
185
|
+
} else {
|
|
186
|
+
reportSafe = { path: [...path], strInfo };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return { vulnInfo, reportSafe };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
instr.getGroupVulnerabilityInfo = function getGroupVulnerabilityInfo(argToCheck, argIdx) {
|
|
196
|
+
let vulnInfo = null;
|
|
197
|
+
let reportSafe = null;
|
|
198
|
+
|
|
199
|
+
if (argIdx !== 1 && isString(argToCheck)) {
|
|
200
|
+
const strInfo = tracker.getData(argToCheck);
|
|
201
|
+
if (strInfo) {
|
|
202
|
+
if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
|
|
203
|
+
vulnInfo = { strInfo };
|
|
204
|
+
} else {
|
|
205
|
+
reportSafe = { strInfo };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { vulnInfo, reportSafe };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!isNonEmptyObject(argToCheck)) return { vulnInfo, reportSafe };
|
|
213
|
+
|
|
214
|
+
traverseValues(argToCheck, (path, _type, value) => {
|
|
215
|
+
const strInfo = tracker.getData(value);
|
|
216
|
+
if (strInfo) {
|
|
217
|
+
if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
|
|
218
|
+
vulnInfo = { path: [...path], strInfo };
|
|
219
|
+
return true; // halts traversal
|
|
220
|
+
} else {
|
|
221
|
+
reportSafe = { path: [...path], strInfo };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return { vulnInfo, reportSafe };
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
function createAroundHook(entity, name, method, getInfoMethod, vulnerableArgIdxs) {
|
|
230
|
+
const argsIdxsToCheck = vulnerableArgIdxs || [0];
|
|
231
|
+
return function(next, data) {
|
|
232
|
+
const { obj, args: origArgs } = data;
|
|
233
|
+
const sourceCtx = sources.getStore()?.assess;
|
|
234
|
+
|
|
235
|
+
if (isLocked(NOSQL_INJECTION_MONGO) || instrumentation.isLocked() || !sourceCtx) {
|
|
236
|
+
return next();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let vulnInfo;
|
|
240
|
+
let reportSafe;
|
|
241
|
+
let vulnArgIdx;
|
|
242
|
+
const safeReports = [];
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
for (const argIdx of argsIdxsToCheck) {
|
|
246
|
+
({ vulnInfo, reportSafe } = getInfoMethod(origArgs[argIdx], argIdx));
|
|
247
|
+
|
|
248
|
+
if (vulnInfo) {
|
|
249
|
+
vulnArgIdx = argIdx;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
if (reportSafe) safeReports.push({ ...reportSafe, argIdx });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!vulnInfo) {
|
|
256
|
+
if (safeReports.length && config.assess.safe_positives.enable) {
|
|
257
|
+
const safeTags = safeReports.map((report) => filterSafeTags(querySafeTags, report.strInfo));
|
|
258
|
+
const strInfo = safeReports.map((report) => {
|
|
259
|
+
const tags = report.path ? getAdjustedQueryTags(report.path, report.strInfo, inspect(origArgs[report.argIdx], { depth: 4 })) : report.strInfo?.tags;
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
value: inspect(origArgs[report.argIdx], { depth: 4 }),
|
|
263
|
+
tags
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
reportSafePositive({
|
|
268
|
+
name,
|
|
269
|
+
ruleId: NOSQL_INJECTION_MONGO,
|
|
270
|
+
safeTags: safeTags.length === 1 ? safeTags[0] : safeTags,
|
|
271
|
+
strInfo: strInfo.length === 1 ? strInfo[0] : strInfo
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return methodsWithNestedCalls.includes(method) ? runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next()) : next();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const { path, strInfo } = vulnInfo;
|
|
279
|
+
const objName = getObjectName(obj, entity);
|
|
280
|
+
const args = origArgs.map((arg, idx) => ({
|
|
281
|
+
value: isString(arg) ? arg : inspect(arg, { depth: 4 }),
|
|
282
|
+
tracked: idx === vulnArgIdx,
|
|
283
|
+
}));
|
|
284
|
+
|
|
285
|
+
const tags = path ? getAdjustedQueryTags(path, strInfo, args[vulnArgIdx].value) : strInfo?.tags;
|
|
286
|
+
const resultVal = args[args.length - 1].value.startsWith('[Function') ? '' : 'Promise';
|
|
287
|
+
const sinkEvent = createSinkEvent({
|
|
288
|
+
args,
|
|
289
|
+
context: `${objName}.${method}(${args.map((a) => a.value)})`,
|
|
290
|
+
history: [strInfo],
|
|
291
|
+
object: {
|
|
292
|
+
tracked: false,
|
|
293
|
+
value: `mongodb.${entity}`,
|
|
294
|
+
},
|
|
295
|
+
name,
|
|
296
|
+
result: {
|
|
297
|
+
tracked: false,
|
|
298
|
+
value: resultVal,
|
|
299
|
+
},
|
|
300
|
+
source: `P${vulnArgIdx}`,
|
|
301
|
+
stacktraceOpts: {
|
|
302
|
+
constructorOpt: data.hooked,
|
|
303
|
+
},
|
|
304
|
+
tags,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (sinkEvent) {
|
|
308
|
+
reportFindings({ ruleId: NOSQL_INJECTION_MONGO, sinkEvent });
|
|
309
|
+
}
|
|
310
|
+
} catch (err) {
|
|
311
|
+
core.logger.error({ name, err }, 'assess sink analysis failed');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return methodsWithNestedCalls.includes(method) ? runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next()) : next();
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
91
318
|
instr.install = function() {
|
|
92
319
|
depHooks.resolve({ name: 'mongodb' }, (mongodb, version) => {
|
|
93
320
|
patchCollection(mongodb, version);
|
|
@@ -99,93 +326,77 @@ module.exports = function(core) {
|
|
|
99
326
|
|
|
100
327
|
function patchCollection(mongodb, version) {
|
|
101
328
|
for (const method of collectionMethods) {
|
|
102
|
-
|
|
103
329
|
const proto = mongodb.Collection.prototype;
|
|
104
330
|
const name = `mongodb.Collection.prototype.${method}`;
|
|
105
331
|
|
|
106
332
|
if (!proto[method]) {
|
|
107
|
-
|
|
108
333
|
logger.trace({ name, version }, 'method not found - skipping instrumentation');
|
|
109
334
|
continue;
|
|
110
335
|
}
|
|
111
336
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
args,
|
|
138
|
-
context: `${objName}.${method}(${args.map((a) => a.value)})`,
|
|
139
|
-
history: [strInfo],
|
|
140
|
-
object: {
|
|
141
|
-
tracked: false,
|
|
142
|
-
value: 'mongodb.Collection',
|
|
143
|
-
},
|
|
144
|
-
name,
|
|
145
|
-
result: {
|
|
146
|
-
tracked: false,
|
|
147
|
-
value: resultVal,
|
|
148
|
-
},
|
|
149
|
-
source: `P${argIdx}`,
|
|
150
|
-
stacktraceOpts: {
|
|
151
|
-
constructorOpt: data.hooked,
|
|
152
|
-
},
|
|
153
|
-
tags,
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
if (sinkEvent) {
|
|
157
|
-
reportFindings({ ruleId: Rule.NOSQL_INJECTION_MONGO, sinkEvent });
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
} catch (err) {
|
|
161
|
-
core.logger.error({ name, err }, 'assess sink analysis failed');
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (method === 'findOne') {
|
|
165
|
-
// `findOne` will call `find` so don't analyze in nested call
|
|
166
|
-
const store = { name, lock: true };
|
|
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;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return next();
|
|
174
|
-
},
|
|
175
|
-
});
|
|
337
|
+
if (method === 'aggregate') {
|
|
338
|
+
patcher.patch(proto, method, {
|
|
339
|
+
name,
|
|
340
|
+
patchType,
|
|
341
|
+
around: createAroundHook('Collection', name, method, instr.getAggregateVulnerabilityInfo)
|
|
342
|
+
});
|
|
343
|
+
} else if (method === 'mapReduce') {
|
|
344
|
+
patcher.patch(proto, method, {
|
|
345
|
+
name,
|
|
346
|
+
patchType,
|
|
347
|
+
around: createAroundHook('Collection', name, method, instr.getMapReduceVulnerabilityInfo, [0, 1, 2])
|
|
348
|
+
});
|
|
349
|
+
} else if (method === 'group') {
|
|
350
|
+
patcher.patch(proto, method, {
|
|
351
|
+
name,
|
|
352
|
+
patchType,
|
|
353
|
+
around: createAroundHook('Collection', name, method, instr.getGroupVulnerabilityInfo, [0, 1, 3])
|
|
354
|
+
});
|
|
355
|
+
} else {
|
|
356
|
+
patcher.patch(proto, method, {
|
|
357
|
+
name,
|
|
358
|
+
patchType,
|
|
359
|
+
around: createAroundHook('Collection', name, method, instr.getQueryVulnerabilityInfo)
|
|
360
|
+
});
|
|
361
|
+
}
|
|
176
362
|
}
|
|
177
363
|
}
|
|
178
364
|
|
|
179
|
-
|
|
180
365
|
function patchDatabase(mongodb, version) {
|
|
181
|
-
|
|
366
|
+
for (const method of dbMethods) {
|
|
367
|
+
const proto = mongodb.Db.prototype;
|
|
368
|
+
const name = `mongodb.Db.prototype.${method}`;
|
|
369
|
+
|
|
370
|
+
if (!proto[method]) {
|
|
371
|
+
logger.trace({ name, version }, 'method not found - skipping instrumentation');
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (method === 'aggregate') {
|
|
376
|
+
patcher.patch(proto, method, {
|
|
377
|
+
name,
|
|
378
|
+
patchType,
|
|
379
|
+
around: createAroundHook('Db', name, method, instr.getAggregateVulnerabilityInfo)
|
|
380
|
+
});
|
|
381
|
+
} else {
|
|
382
|
+
patcher.patch(proto, method, {
|
|
383
|
+
name,
|
|
384
|
+
patchType,
|
|
385
|
+
around: createAroundHook('Db', name, method, instr.getQueryVulnerabilityInfo)
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
182
389
|
}
|
|
183
390
|
|
|
184
|
-
function getObjectName(obj) {
|
|
391
|
+
function getObjectName(obj, entity) {
|
|
185
392
|
let name = '';
|
|
186
393
|
name += obj.s?.namespace?.db || 'db';
|
|
187
|
-
|
|
188
|
-
|
|
394
|
+
|
|
395
|
+
if (entity !== 'Db') {
|
|
396
|
+
name += '.';
|
|
397
|
+
name += obj.s?.namespace?.collection || 'collection';
|
|
398
|
+
}
|
|
399
|
+
|
|
189
400
|
return name;
|
|
190
401
|
}
|
|
191
402
|
|
|
@@ -193,6 +404,10 @@ module.exports = function(core) {
|
|
|
193
404
|
const { tags } = strInfo;
|
|
194
405
|
let idx = -1;
|
|
195
406
|
for (const str of [...path, strInfo.value]) {
|
|
407
|
+
// This is the case with the `.aggregate` method
|
|
408
|
+
// where the argument is an array
|
|
409
|
+
if (str === 0) continue;
|
|
410
|
+
|
|
196
411
|
idx = argString.indexOf(str, idx);
|
|
197
412
|
if (idx == -1) {
|
|
198
413
|
idx = -1;
|
|
@@ -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
|
});
|