@contrast/agent 4.22.1 → 4.24.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/assess/models/base-event.js +11 -1
- package/lib/assess/models/call-context.js +20 -0
- package/lib/assess/propagators/JSON/stringify.js +3 -1
- package/lib/assess/sinks/dynamo.js +1 -0
- package/lib/core/async-storage/index.js +1 -1
- package/lib/core/express/index.js +75 -29
- package/lib/protect/sinks/mongodb.js +77 -65
- package/lib/reporter/models/finding/event.js +9 -6
- package/lib/reporter/translations/to-protobuf/dtm/finding.js +11 -7
- package/package.json +2 -2
|
@@ -112,7 +112,17 @@ class BaseEvent {
|
|
|
112
112
|
this._expanded = true;
|
|
113
113
|
this.thread = process.pid;
|
|
114
114
|
if (this.source === 'P') {
|
|
115
|
-
|
|
115
|
+
const numArgs = this.context.hasArgsTracked.length;
|
|
116
|
+
if (numArgs === 1) {
|
|
117
|
+
this.source = 'P0'
|
|
118
|
+
} else {
|
|
119
|
+
this.source = '';
|
|
120
|
+
for (let i = 0; i < numArgs; i++) {
|
|
121
|
+
if (this.context.hasArgsTracked[i]) {
|
|
122
|
+
this.source += `P${i},`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
if (this.signature) {
|
|
@@ -103,6 +103,25 @@ module.exports = class CallContext {
|
|
|
103
103
|
return !!(str && typeof str === 'object' && str[PROXY_TARGET]);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
static hasTrackedArg(arg, iteration = 0) {
|
|
107
|
+
if (tracker.getData(arg)) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (arg && typeof arg === 'object') {
|
|
112
|
+
|
|
113
|
+
for (const key in arg) {
|
|
114
|
+
if (tracker.getData(arg[key])) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (arg[key] && typeof arg[key] === 'object' && iteration < 100) {
|
|
118
|
+
return CallContext.hasTrackedArg(arg[key], iteration += 1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
106
125
|
static getDisplayRange(arg, orgArg = arg, iteration = 0) {
|
|
107
126
|
if (tracker.getData(arg)) {
|
|
108
127
|
return new TagRange(0, arg.length - 1, 'untrusted');
|
|
@@ -144,6 +163,7 @@ module.exports = class CallContext {
|
|
|
144
163
|
set args(args) {
|
|
145
164
|
this.__args = args.map(CallContext.valueString);
|
|
146
165
|
this.argsTracked = args.map((arg) => CallContext.isTracked(arg));
|
|
166
|
+
this.hasArgsTracked = args.map((arg) => CallContext.hasTrackedArg(arg));
|
|
147
167
|
this.argsDisplayRanges = args.map((arg) => CallContext.getDisplayRange(arg));
|
|
148
168
|
}
|
|
149
169
|
|
|
@@ -331,7 +331,9 @@ module.exports.handle = function() {
|
|
|
331
331
|
// always exist, even if an empty array, for production code.
|
|
332
332
|
if (metadata.sourceEvents && !metadata.sourceEvents.length) {
|
|
333
333
|
const { sourceEvents, braned } = data.metadata;
|
|
334
|
-
|
|
334
|
+
// If it is a source membrane (could be deserialization membrane), then
|
|
335
|
+
// we need to get a source event.
|
|
336
|
+
if (braned && braned.membrane && braned.membrane.makeReqSourceEvent) {
|
|
335
337
|
sourceEvents.push(
|
|
336
338
|
braned.membrane.makeReqSourceEvent(data.result.length)
|
|
337
339
|
);
|
|
@@ -59,6 +59,7 @@ class ExpressFramework {
|
|
|
59
59
|
// Express middleware error handler.
|
|
60
60
|
this.errorHandler = ExpressFramework.ContrastErrorHandler.bind(this);
|
|
61
61
|
moduleHook.resolve({ name: 'express' }, this.hookExpress.bind(this));
|
|
62
|
+
moduleHook.resolve({ name: 'body-parser' }, this.hookBodyParser.bind(this));
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
/**
|
|
@@ -227,6 +228,80 @@ class ExpressFramework {
|
|
|
227
228
|
agentEmitter.on(HTTP_EVENTS.SERVER_LISTEN, this.onServerListen.bind(this));
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
hookBodyParser(bodyParser) {
|
|
232
|
+
const instrumentation = this;
|
|
233
|
+
const origBodyParser = bodyParser;
|
|
234
|
+
|
|
235
|
+
const { json: origJson, raw: origRaw, text: origText, urlencoded: origUrlencoded } = bodyParser;
|
|
236
|
+
const fnArr = [
|
|
237
|
+
{
|
|
238
|
+
key: 'json',
|
|
239
|
+
original: origJson,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
key: 'raw',
|
|
243
|
+
original: origRaw,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
key: 'text',
|
|
247
|
+
original: origText,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
key: 'urlencoded',
|
|
251
|
+
original: origUrlencoded,
|
|
252
|
+
}
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
bodyParser = function bodyParser(...args) {
|
|
256
|
+
const parser = origBodyParser(...args);
|
|
257
|
+
const hookedParser = function(req, res, next) {
|
|
258
|
+
parser(req, res, instrumentation.contrastNext(req, res, next, 'bodyParser'));
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
Object.defineProperty(hookedParser, 'name', {
|
|
262
|
+
value: 'bodyParser'
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return hookedParser;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
fnArr.forEach((fn) => {
|
|
269
|
+
const fnName = `bodyParser.${fn.key}`;
|
|
270
|
+
function contrastHooked(...args) {
|
|
271
|
+
const parser = fn.original(...args);
|
|
272
|
+
const hookedParser = function (req, res, next) {
|
|
273
|
+
parser(req, res, instrumentation.contrastNext(req, res, next, fnName));
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
Object.defineProperty(hookedParser, 'name', {
|
|
277
|
+
value: `${fn.key}Parser`
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return hookedParser;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
Object.defineProperty(bodyParser, fn.key, {
|
|
284
|
+
configurable: true,
|
|
285
|
+
enumerable: true,
|
|
286
|
+
get: () => contrastHooked,
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return bodyParser;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
contrastNext(req, res, origNext, fnName) {
|
|
294
|
+
return function next() {
|
|
295
|
+
if (fnName == 'bodyParser.json') {
|
|
296
|
+
agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.JSON_VALUE);
|
|
297
|
+
} else {
|
|
298
|
+
agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.BODY);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
origNext();
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
230
305
|
onServerListen(args, server) {
|
|
231
306
|
if (!this.serversSeen.has(server)) {
|
|
232
307
|
logger.debug('ignore server.listen: non-express handler');
|
|
@@ -316,35 +391,6 @@ class ExpressFramework {
|
|
|
316
391
|
next();
|
|
317
392
|
}, 'multerMiddleware');
|
|
318
393
|
|
|
319
|
-
// ... body-parser ...............................................
|
|
320
|
-
this.useAfter(function ContrastRawBodyParsed(req, res, next) {
|
|
321
|
-
agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.BODY);
|
|
322
|
-
next();
|
|
323
|
-
}, 'rawParser');
|
|
324
|
-
|
|
325
|
-
// ... bodyParser in Sails Framework ............................
|
|
326
|
-
this.useAfter(function ContrastBodyParsed(req, res, next) {
|
|
327
|
-
if (req._sails && req.body) {
|
|
328
|
-
agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.BODY);
|
|
329
|
-
}
|
|
330
|
-
next();
|
|
331
|
-
}, '_parseHTTPBody');
|
|
332
|
-
|
|
333
|
-
this.useAfter(function ContrastTextBodyParsed(req, res, next) {
|
|
334
|
-
agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.BODY);
|
|
335
|
-
next();
|
|
336
|
-
}, 'textParser');
|
|
337
|
-
|
|
338
|
-
this.useAfter(function ContrastBodyParsed(req, res, next) {
|
|
339
|
-
agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.BODY);
|
|
340
|
-
next();
|
|
341
|
-
}, 'urlencodedParser');
|
|
342
|
-
|
|
343
|
-
this.useAfter(function ContrastJSONParsed(req, res, next) {
|
|
344
|
-
agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.JSON_VALUE);
|
|
345
|
-
next();
|
|
346
|
-
}, 'jsonParser');
|
|
347
|
-
|
|
348
394
|
// ... cookie-parser .............................................
|
|
349
395
|
this.useAfter(function ContrastCookiesParsed(req, res, next) {
|
|
350
396
|
agentEmitter.emit(
|
|
@@ -16,13 +16,11 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
16
16
|
|
|
17
17
|
const _ = require('lodash');
|
|
18
18
|
const semver = require('semver');
|
|
19
|
-
const constants = require('../../constants');
|
|
20
|
-
const BaseSensor = require('../../hooks/frameworks/base');
|
|
21
19
|
const patcher = require('../../hooks/patcher');
|
|
22
|
-
const
|
|
20
|
+
const BaseSensor = require('../../hooks/frameworks/base');
|
|
21
|
+
const { PATCH_TYPES, SINK_TYPES } = require('../../constants');
|
|
23
22
|
const { emitSinkEvent } = require('../../hooks/frameworks/common');
|
|
24
23
|
|
|
25
|
-
const { SINK_TYPES } = constants;
|
|
26
24
|
const ID = 'mongodb';
|
|
27
25
|
|
|
28
26
|
function getCursorQueryData(args, version) {
|
|
@@ -61,84 +59,98 @@ class MongoDBSensor extends BaseSensor {
|
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
install({ ModuleHook }) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
62
|
+
const v4MethodsWithFilter = [
|
|
63
|
+
'updateOne',
|
|
64
|
+
'replaceOne',
|
|
65
|
+
'updateMany',
|
|
66
|
+
'deleteOne',
|
|
67
|
+
'deleteMany',
|
|
68
|
+
'findOneAndDelete',
|
|
69
|
+
'findOneAndReplace',
|
|
70
|
+
'findOneAndUpdate',
|
|
71
|
+
'countDocuments',
|
|
72
|
+
'count',
|
|
73
|
+
'distinct',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
ModuleHook.resolve({ name: ID, version: '<4.0.0' }, (mongodb, { version }) => {
|
|
77
|
+
['command', 'cursor'].forEach(method => {
|
|
78
|
+
patcher.patch(mongodb.CoreServer.prototype, method, {
|
|
79
|
+
alwaysRun: true,
|
|
80
|
+
name: `mongodb.CoreServer.prototype.${method}`,
|
|
81
|
+
patchType: PATCH_TYPES.PROTECT_SINK,
|
|
82
|
+
pre: (wrapCtx) => {
|
|
83
|
+
emitSinkEvent(
|
|
84
|
+
getCursorQueryData(wrapCtx.args, version),
|
|
85
|
+
SINK_TYPES.NOSQL_QUERY,
|
|
86
|
+
ID
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
91
90
|
});
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
emitSinkEvent(eData, SINK_TYPES.NOSQL_QUERY, ID);
|
|
92
|
+
['remove', 'update'].forEach(method => {
|
|
93
|
+
patcher.patch(mongodb.CoreServer.prototype, method, {
|
|
94
|
+
alwaysRun: true,
|
|
95
|
+
name: 'mongodb.CoreServer.prototype.remove',
|
|
96
|
+
patchType: PATCH_TYPES.PROTECT_SINK,
|
|
97
|
+
pre: (wrapCtx) => {
|
|
98
|
+
const ops = Array.isArray(wrapCtx.args[1]) ? wrapCtx.args[1] : [wrapCtx.args[1]];
|
|
99
|
+
|
|
100
|
+
for (const op of ops) {
|
|
101
|
+
const eData = getOpQueryData(op);
|
|
102
|
+
if (eData) {
|
|
103
|
+
emitSinkEvent(eData, SINK_TYPES.NOSQL_QUERY, ID);
|
|
104
|
+
}
|
|
107
105
|
}
|
|
108
106
|
}
|
|
109
|
-
}
|
|
107
|
+
});
|
|
110
108
|
});
|
|
111
109
|
|
|
112
|
-
|
|
113
|
-
patcher.patch(mongodb.CoreServer.prototype, 'update', {
|
|
110
|
+
patcher.patch(mongodb.Db.prototype, 'eval', {
|
|
114
111
|
alwaysRun: true,
|
|
115
|
-
name: 'mongodb.
|
|
112
|
+
name: 'mongodb.Db.prototype.eval',
|
|
116
113
|
patchType: PATCH_TYPES.PROTECT_SINK,
|
|
117
|
-
pre: (
|
|
118
|
-
|
|
119
|
-
? data.args[1]
|
|
120
|
-
: [data.args[1]];
|
|
121
|
-
|
|
122
|
-
for (const op of ops) {
|
|
123
|
-
const eData = getOpQueryData(op);
|
|
124
|
-
if (eData) {
|
|
125
|
-
emitSinkEvent(eData, SINK_TYPES.NOSQL_QUERY, ID);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
114
|
+
pre: (wrapCtx) => {
|
|
115
|
+
emitSinkEvent(wrapCtx.args[0], SINK_TYPES.NOSQL_QUERY, ID);
|
|
128
116
|
}
|
|
129
117
|
});
|
|
118
|
+
});
|
|
130
119
|
|
|
131
|
-
|
|
132
|
-
|
|
120
|
+
ModuleHook.resolve({ name: ID, version: '>=4.0.0' }, (mongodb) => {
|
|
121
|
+
v4MethodsWithFilter.forEach((method) => {
|
|
122
|
+
patcher.patch(mongodb.Collection.prototype, method, {
|
|
123
|
+
alwaysRun: true,
|
|
124
|
+
name: `mongodb.Collection.prototype.${method}`,
|
|
125
|
+
patchType: PATCH_TYPES.PROTECT_SINK,
|
|
126
|
+
pre: (wrapCtx) => {
|
|
127
|
+
const value = typeof wrapCtx.args[0] == 'function' ? null : wrapCtx.args[0];
|
|
128
|
+
emitSinkEvent(value, SINK_TYPES.NOSQL_QUERY, ID);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
patcher.patch(mongodb.Db.prototype, 'command', {
|
|
133
134
|
alwaysRun: true,
|
|
134
|
-
name: 'mongodb.Db.prototype',
|
|
135
|
+
name: 'mongodb.Db.prototype.command',
|
|
135
136
|
patchType: PATCH_TYPES.PROTECT_SINK,
|
|
136
|
-
pre: (
|
|
137
|
-
|
|
137
|
+
pre: (wrapCtx) => {
|
|
138
|
+
const value = wrapCtx.args[0] && wrapCtx.args[0].filter;
|
|
139
|
+
emitSinkEvent(value, SINK_TYPES.NOSQL_QUERY, ID);
|
|
138
140
|
}
|
|
139
141
|
});
|
|
140
142
|
});
|
|
141
143
|
|
|
144
|
+
ModuleHook.resolve({ name: ID, file: 'lib/cursor/find_cursor', version: '>=4.0.0' }, (cursor) => patcher.patch(cursor, 'FindCursor', {
|
|
145
|
+
alwaysRun: true,
|
|
146
|
+
name: 'mongodb.FindCursor',
|
|
147
|
+
patchType: PATCH_TYPES.PROTECT_SINK,
|
|
148
|
+
pre: (wrapCtx) => {
|
|
149
|
+
const value = wrapCtx.args[2];
|
|
150
|
+
emitSinkEvent(value, SINK_TYPES.NOSQL_QUERY, ID);
|
|
151
|
+
}
|
|
152
|
+
}));
|
|
153
|
+
|
|
142
154
|
return this;
|
|
143
155
|
}
|
|
144
156
|
}
|
|
@@ -102,12 +102,15 @@ class Event {
|
|
|
102
102
|
this.args.push(
|
|
103
103
|
new ObjectDTM(event.context.args[i], event.context.argsTracked[i])
|
|
104
104
|
);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
const displayRanges = event.context.argsDisplayRanges.filter((tagRange) => Object.keys(tagRange).length);;
|
|
109
|
+
|
|
110
|
+
if (displayRanges.length) {
|
|
111
|
+
// If displayRanges is non-empty (=/= [{}]), use that instead
|
|
112
|
+
// since it's more accurate when reporting
|
|
113
|
+
event.tagRanges = displayRanges;
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
if (event.code) {
|
|
@@ -29,15 +29,19 @@ module.exports = function Finding(details = {}) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const routes = RoutesListWithObservations(details.routes);
|
|
32
|
+
const properties = objToMap(details.properties).map(
|
|
33
|
+
// ensure string values
|
|
34
|
+
([key, value]) => [key, String(value)]
|
|
35
|
+
);
|
|
32
36
|
|
|
33
37
|
return new dtm.Finding({
|
|
34
|
-
0: String(details.hash || ''), //
|
|
35
|
-
2: details.ruleId, //
|
|
36
|
-
5:
|
|
37
|
-
6: mapToModelArray(TraceEvent, details.events), //
|
|
38
|
-
7: preflight, //
|
|
39
|
-
8: details.tags, //
|
|
40
|
-
9: details.version, //
|
|
38
|
+
0: String(details.hash || ''), // 1 hash_code
|
|
39
|
+
2: details.ruleId, // 3 rule_id
|
|
40
|
+
5: properties, // 6 properties
|
|
41
|
+
6: mapToModelArray(TraceEvent, details.events), // 7 events
|
|
42
|
+
7: preflight, // 8 preflight
|
|
43
|
+
8: details.tags, // 9 tags
|
|
44
|
+
9: details.version, // 10 version
|
|
41
45
|
10: routes // 11 routes
|
|
42
46
|
});
|
|
43
47
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/agent",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.24.0",
|
|
4
4
|
"description": "Node.js security instrumentation by Contrast Security",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"security",
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
"mock-fs": "^5.1.2",
|
|
164
164
|
"mongodb": "file:test/mock/mongodb",
|
|
165
165
|
"mongodb-npm": "npm:mongodb@^3.6.5",
|
|
166
|
-
"mongoose": "^6.
|
|
166
|
+
"mongoose": "^6.4.6",
|
|
167
167
|
"mustache": "^3.0.1",
|
|
168
168
|
"mysql": "file:test/mock/mysql",
|
|
169
169
|
"mysql2": "file:test/mock/mysql2",
|