@contrast/assess 1.7.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 +2 -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/sinks/install/mongodb.js +247 -149
- package/lib/dataflow/sources/handler.js +140 -26
- package/lib/dataflow/sources/index.js +2 -7
- package/lib/dataflow/sources/install/body-parser1.js +19 -6
- package/lib/dataflow/sources/install/express/index.js +4 -1
- 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 +2 -2
|
@@ -47,11 +47,16 @@ const collectionMethods = [
|
|
|
47
47
|
'updateMany',
|
|
48
48
|
'deleteOne',
|
|
49
49
|
'deleteMany',
|
|
50
|
+
'aggregate',
|
|
51
|
+
'mapReduce',
|
|
52
|
+
'group'
|
|
50
53
|
];
|
|
51
54
|
const dbMethods = [
|
|
52
55
|
'command',
|
|
53
|
-
'eval'
|
|
56
|
+
'eval',
|
|
57
|
+
'aggregate'
|
|
54
58
|
];
|
|
59
|
+
const methodsWithNestedCalls = ['findOne', 'eval', 'group'];
|
|
55
60
|
|
|
56
61
|
const querySafeTags = [
|
|
57
62
|
ALPHANUM_SPACE_HYPHEN,
|
|
@@ -80,7 +85,7 @@ module.exports = function(core) {
|
|
|
80
85
|
const inspect = patcher.unwrap(util.inspect);
|
|
81
86
|
const instr = core.assess.dataflow.sinks.mongodb = {};
|
|
82
87
|
|
|
83
|
-
instr.
|
|
88
|
+
instr.getQueryVulnerabilityInfo = function getQueryVulnerabilityInfo(query) {
|
|
84
89
|
let vulnInfo = null;
|
|
85
90
|
let reportSafe = null;
|
|
86
91
|
|
|
@@ -103,10 +108,10 @@ module.exports = function(core) {
|
|
|
103
108
|
const strInfo = tracker.getData(value);
|
|
104
109
|
if (strInfo) {
|
|
105
110
|
if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
|
|
106
|
-
vulnInfo = { path, strInfo };
|
|
111
|
+
vulnInfo = { path: [...path], strInfo };
|
|
107
112
|
return true; // halts traversal
|
|
108
113
|
} else {
|
|
109
|
-
reportSafe = { path, strInfo };
|
|
114
|
+
reportSafe = { path: [...path], strInfo };
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
117
|
});
|
|
@@ -114,6 +119,202 @@ module.exports = function(core) {
|
|
|
114
119
|
return { vulnInfo, reportSafe };
|
|
115
120
|
};
|
|
116
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
|
+
|
|
117
318
|
instr.install = function() {
|
|
118
319
|
depHooks.resolve({ name: 'mongodb' }, (mongodb, version) => {
|
|
119
320
|
patchCollection(mongodb, version);
|
|
@@ -133,78 +334,31 @@ module.exports = function(core) {
|
|
|
133
334
|
continue;
|
|
134
335
|
}
|
|
135
336
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return method === 'findOne' ? runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next()) : next();
|
|
162
|
-
}
|
|
163
|
-
|
|
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 });
|
|
195
|
-
}
|
|
196
|
-
} catch (err) {
|
|
197
|
-
core.logger.error({ name, err }, 'assess sink analysis failed');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (method === 'findOne') {
|
|
201
|
-
// `findOne` will call `find` so don't analyze in nested call
|
|
202
|
-
return runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next());
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return next();
|
|
206
|
-
},
|
|
207
|
-
});
|
|
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
|
+
}
|
|
208
362
|
}
|
|
209
363
|
}
|
|
210
364
|
|
|
@@ -218,79 +372,19 @@ module.exports = function(core) {
|
|
|
218
372
|
continue;
|
|
219
373
|
}
|
|
220
374
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
});
|
|
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
|
+
}
|
|
294
388
|
}
|
|
295
389
|
}
|
|
296
390
|
|
|
@@ -310,6 +404,10 @@ module.exports = function(core) {
|
|
|
310
404
|
const { tags } = strInfo;
|
|
311
405
|
let idx = -1;
|
|
312
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
|
+
|
|
313
411
|
idx = argString.indexOf(str, idx);
|
|
314
412
|
if (idx == -1) {
|
|
315
413
|
idx = -1;
|
|
@@ -19,7 +19,6 @@ const {
|
|
|
19
19
|
InputType,
|
|
20
20
|
DataflowTag,
|
|
21
21
|
isString,
|
|
22
|
-
traverseValues
|
|
23
22
|
} = require('@contrast/common');
|
|
24
23
|
|
|
25
24
|
module.exports = function(core) {
|
|
@@ -28,17 +27,17 @@ module.exports = function(core) {
|
|
|
28
27
|
dataflow: {
|
|
29
28
|
sources,
|
|
30
29
|
tracker,
|
|
31
|
-
eventFactory
|
|
30
|
+
eventFactory
|
|
32
31
|
}
|
|
33
32
|
},
|
|
34
|
-
createSnapshot,
|
|
35
33
|
config,
|
|
34
|
+
createSnapshot,
|
|
36
35
|
logger,
|
|
37
36
|
} = core;
|
|
38
37
|
|
|
39
38
|
const emptyStack = Object.freeze([]);
|
|
40
39
|
|
|
41
|
-
sources.createTags = function createTags({ inputType,
|
|
40
|
+
sources.createTags = function createTags({ inputType, fieldName = '', value }) {
|
|
42
41
|
if (!value?.length) {
|
|
43
42
|
return null;
|
|
44
43
|
}
|
|
@@ -48,25 +47,30 @@ module.exports = function(core) {
|
|
|
48
47
|
[DataflowTag.UNTRUSTED]: [0, stop]
|
|
49
48
|
};
|
|
50
49
|
|
|
51
|
-
if (inputType === InputType.HEADER &&
|
|
50
|
+
if (inputType === InputType.HEADER && fieldName.toLowerCase() === 'referer') {
|
|
52
51
|
tags[DataflowTag.HEADER] = [0, stop];
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
return tags;
|
|
56
55
|
};
|
|
57
56
|
|
|
57
|
+
sources.createStacktrace = function(stacktraceOpts) {
|
|
58
|
+
return config.assess.stacktraces === 'NONE'
|
|
59
|
+
? emptyStack
|
|
60
|
+
: createSnapshot(stacktraceOpts)();
|
|
61
|
+
};
|
|
62
|
+
|
|
58
63
|
sources.handle = function({
|
|
59
64
|
context,
|
|
65
|
+
keys,
|
|
60
66
|
name,
|
|
61
67
|
inputType = InputType.UNKNOWN,
|
|
62
68
|
stacktraceOpts,
|
|
63
69
|
data,
|
|
64
|
-
sourceContext
|
|
70
|
+
sourceContext,
|
|
65
71
|
}) {
|
|
66
72
|
if (!data) return;
|
|
67
73
|
|
|
68
|
-
const max = config.assess.max_context_source_events;
|
|
69
|
-
|
|
70
74
|
if (!sourceContext) {
|
|
71
75
|
core.logger.trace({ inputType, name }, 'skipping assess source handling - no request context');
|
|
72
76
|
return null;
|
|
@@ -76,31 +80,60 @@ module.exports = function(core) {
|
|
|
76
80
|
context = inputType;
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
const max = config.assess.max_context_source_events;
|
|
84
|
+
let _data = data;
|
|
79
85
|
let stack;
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
if (keys) {
|
|
88
|
+
_data = {};
|
|
89
|
+
for (const key of keys) {
|
|
90
|
+
_data[key] = data[key];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createEvent({ fieldName, pathName, value }) {
|
|
95
|
+
// create the stacktrace once per call to .handle()
|
|
96
|
+
stack || (stack = sources.createStacktrace(stacktraceOpts));
|
|
97
|
+
return eventFactory.createSourceEvent({
|
|
98
|
+
context: `${context}.${pathName}`,
|
|
99
|
+
name,
|
|
100
|
+
fieldName,
|
|
101
|
+
pathName,
|
|
102
|
+
stack,
|
|
103
|
+
inputType,
|
|
104
|
+
tags: sources.createTags({ inputType, fieldName, value }),
|
|
105
|
+
result: { tracked: true, value },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (Buffer.isBuffer(data) && !tracker.getData(data)) {
|
|
110
|
+
const event = createEvent({ pathName: 'body', value: data, fieldName: '' });
|
|
111
|
+
if (event) {
|
|
112
|
+
tracker.track(data, event);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
traverse(_data, (path, fieldName, value, obj) => {
|
|
118
|
+
const pathName = path.join('.');
|
|
119
|
+
|
|
82
120
|
if (sourceContext.sourceEventsCount >= max) {
|
|
83
121
|
core.logger.trace({ inputType, name }, 'exiting assess source handling - %s max events exceeded', max);
|
|
84
122
|
return true;
|
|
85
123
|
}
|
|
86
124
|
|
|
87
125
|
if (isString(value) && value.length) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
pathName,
|
|
98
|
-
stack,
|
|
99
|
-
inputType,
|
|
100
|
-
tags: sources.createTags({ inputType, key, value }),
|
|
101
|
-
result: { tracked: true, value },
|
|
102
|
-
});
|
|
126
|
+
const strInfo = tracker.getData(value);
|
|
127
|
+
|
|
128
|
+
if (strInfo) {
|
|
129
|
+
// TODO: confirm this "layering-on" approach is what we want
|
|
130
|
+
// when the value is tracked the handler wins out and we "re-tracks" the value with new source
|
|
131
|
+
// event metadata. without this step tracker would complain about value already being tracked.
|
|
132
|
+
// alternatively we could treat this more like a propagation event and update existing metadata.
|
|
133
|
+
value = strInfo.value;
|
|
134
|
+
}
|
|
103
135
|
|
|
136
|
+
const event = createEvent({ pathName, value, fieldName });
|
|
104
137
|
if (!event) {
|
|
105
138
|
core.logger.warn({ inputType, name, pathName, value }, 'unable to create source event');
|
|
106
139
|
return;
|
|
@@ -108,15 +141,96 @@ module.exports = function(core) {
|
|
|
108
141
|
|
|
109
142
|
const { extern } = tracker.track(value, event);
|
|
110
143
|
if (extern) {
|
|
111
|
-
logger.trace({ extern,
|
|
112
|
-
obj[
|
|
144
|
+
logger.trace({ extern, fieldName, name, inputType }, 'tracked');
|
|
145
|
+
obj[fieldName] = extern;
|
|
146
|
+
|
|
113
147
|
sourceContext.sourceEventsCount++;
|
|
114
148
|
}
|
|
149
|
+
} else if (Buffer.isBuffer(value) && !tracker.getData(value)) {
|
|
150
|
+
const event = createEvent({ pathName, value, fieldName });
|
|
151
|
+
if (event) {
|
|
152
|
+
tracker.track(value, event);
|
|
153
|
+
} else {
|
|
154
|
+
core.logger.warn({ inputType, name, pathName, value }, 'unable to create source event');
|
|
155
|
+
}
|
|
115
156
|
}
|
|
116
157
|
});
|
|
117
158
|
|
|
159
|
+
if (keys) {
|
|
160
|
+
for (const key of keys) {
|
|
161
|
+
data[key] = _data[key];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
118
165
|
return data;
|
|
119
166
|
};
|
|
120
167
|
|
|
121
168
|
return sources;
|
|
122
169
|
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A custom traversal function for handling source value tracking efficiently.
|
|
173
|
+
* Implementation was adapted from traversal methods in @contrast/common.
|
|
174
|
+
* @param {any} target object to traverse
|
|
175
|
+
* @param {function} cb function<path, key, value, obj>
|
|
176
|
+
* @param {string[]} path path of node being visted; constructed of nested keys
|
|
177
|
+
* @param {boolean} halt whether to halt traversal; determined by callback
|
|
178
|
+
* @param {Set} visited used to dedupe circular references
|
|
179
|
+
*/
|
|
180
|
+
function traverse(target, cb, path = [], visited = new Set()) {
|
|
181
|
+
if (isTraversable(target)) {
|
|
182
|
+
for (const key in target) {
|
|
183
|
+
path.push(key);
|
|
184
|
+
|
|
185
|
+
const value = target[key];
|
|
186
|
+
|
|
187
|
+
if (visited.has(value)) {
|
|
188
|
+
path.pop();
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (isVisitable(value)) {
|
|
193
|
+
const halt = cb(path, key, value, target) === false;
|
|
194
|
+
if (halt) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (isTraversable(value)) {
|
|
200
|
+
visited.add(value);
|
|
201
|
+
traverse(value, cb, path, visited);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
path.pop();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Visit strings, buffers, basic objects and arrays.
|
|
211
|
+
* @param {any} value the value to check
|
|
212
|
+
* @returns {boolean}
|
|
213
|
+
*/
|
|
214
|
+
function isVisitable(value) {
|
|
215
|
+
if (!value) return false;
|
|
216
|
+
|
|
217
|
+
return value.constructor?.name === 'String' ||
|
|
218
|
+
value.constructor?.name === 'Buffer' ||
|
|
219
|
+
(typeof value === 'object' && !value.constructor);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The criteria for traversal is a strict as possible. We only traverse plain
|
|
224
|
+
* objects and arrays and objects created via Object.create(null).
|
|
225
|
+
* @param {any} value the value to check
|
|
226
|
+
* @returns {boolean}
|
|
227
|
+
*/
|
|
228
|
+
function isTraversable(value) {
|
|
229
|
+
if (!value || typeof value !== 'object') return false;
|
|
230
|
+
|
|
231
|
+
return value.constructor?.name === 'Object' ||
|
|
232
|
+
value.constructor?.name === 'Array' ||
|
|
233
|
+
!value.constructor;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports.traverse = traverse;
|