@contrast/protect 1.8.1 → 1.9.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/error-handlers/install/express4.js +3 -3
- package/lib/error-handlers/install/fastify.js +1 -1
- package/lib/error-handlers/install/hapi.js +1 -1
- package/lib/error-handlers/install/koa2.js +1 -1
- package/lib/hardening/handlers.js +5 -4
- package/lib/index.d.ts +5 -2
- package/lib/index.js +2 -2
- package/lib/input-analysis/handlers.js +26 -25
- package/lib/input-analysis/index.js +2 -2
- package/lib/input-tracing/handlers/index.js +17 -15
- package/lib/input-tracing/index.js +2 -2
- package/lib/input-tracing/install/child-process.js +24 -24
- package/lib/input-tracing/install/function.js +1 -1
- package/lib/input-tracing/install/mongodb.js +148 -165
- package/lib/make-source-context.js +6 -13
- package/lib/semantic-analysis/handlers.js +17 -13
- package/lib/semantic-analysis/index.js +2 -5
- package/lib/throw-security-exception.js +1 -3
- package/package.json +5 -5
|
@@ -44,7 +44,7 @@ module.exports = function(core) {
|
|
|
44
44
|
const isSecurityException = SecurityException.isSecurityException(err);
|
|
45
45
|
|
|
46
46
|
if (isSecurityException && sourceContext) {
|
|
47
|
-
const blockInfo = sourceContext.
|
|
47
|
+
const blockInfo = sourceContext.securityException;
|
|
48
48
|
|
|
49
49
|
sourceContext.block(...blockInfo);
|
|
50
50
|
return;
|
|
@@ -70,7 +70,7 @@ module.exports = function(core) {
|
|
|
70
70
|
const isSecurityException = SecurityException.isSecurityException(err);
|
|
71
71
|
|
|
72
72
|
if (isSecurityException && sourceContext) {
|
|
73
|
-
const blockInfo = sourceContext.
|
|
73
|
+
const blockInfo = sourceContext.securityException;
|
|
74
74
|
|
|
75
75
|
sourceContext.block(...blockInfo);
|
|
76
76
|
return;
|
|
@@ -123,7 +123,7 @@ module.exports = function(core) {
|
|
|
123
123
|
const isSecurityException = SecurityException.isSecurityException(err);
|
|
124
124
|
|
|
125
125
|
if (isSecurityException && sourceContext) {
|
|
126
|
-
const blockInfo = sourceContext.
|
|
126
|
+
const blockInfo = sourceContext.securityException;
|
|
127
127
|
|
|
128
128
|
sourceContext.block(...blockInfo);
|
|
129
129
|
return;
|
|
@@ -62,7 +62,7 @@ module.exports = function(core) {
|
|
|
62
62
|
if (!sourceContext) {
|
|
63
63
|
normalHandler.call(this, err, request, reply);
|
|
64
64
|
} else {
|
|
65
|
-
const blockInfo = sourceContext.
|
|
65
|
+
const blockInfo = sourceContext.securityException;
|
|
66
66
|
sourceContext.block(...blockInfo);
|
|
67
67
|
}
|
|
68
68
|
} else {
|
|
@@ -39,7 +39,7 @@ module.exports = function (core) {
|
|
|
39
39
|
const isSecurityException = SecurityException.isSecurityException(err);
|
|
40
40
|
|
|
41
41
|
if (isSecurityException && sourceContext && err.output.statusCode !== 403) {
|
|
42
|
-
const [mode, ruleId] = sourceContext.
|
|
42
|
+
const [mode, ruleId] = sourceContext.securityException;
|
|
43
43
|
|
|
44
44
|
err.output.statusCode = 403;
|
|
45
45
|
err.reformat();
|
|
@@ -46,7 +46,7 @@ module.exports = function(core) {
|
|
|
46
46
|
|
|
47
47
|
if (isSecurityException && sourceContext) {
|
|
48
48
|
data.obj.body = '';
|
|
49
|
-
const blockInfo = sourceContext.
|
|
49
|
+
const blockInfo = sourceContext.securityException;
|
|
50
50
|
sourceContext.block(...blockInfo);
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
@@ -29,9 +29,9 @@ module.exports = function(core) {
|
|
|
29
29
|
} = core;
|
|
30
30
|
|
|
31
31
|
function getResults(sourceContext, ruleId) {
|
|
32
|
-
let results = sourceContext.
|
|
32
|
+
let results = sourceContext.resultsMap[ruleId];
|
|
33
33
|
if (!results) {
|
|
34
|
-
results = sourceContext.
|
|
34
|
+
results = sourceContext.resultsMap[ruleId] = [];
|
|
35
35
|
}
|
|
36
36
|
return results;
|
|
37
37
|
}
|
|
@@ -51,13 +51,14 @@ module.exports = function(core) {
|
|
|
51
51
|
|
|
52
52
|
captureStacktrace(sinkContext, stacktraceData);
|
|
53
53
|
results.push({
|
|
54
|
+
value: sinkContext.value,
|
|
54
55
|
blocked,
|
|
55
|
-
|
|
56
|
+
exploitMetadata: [{ deserializer: name, command: false }],
|
|
56
57
|
sinkContext,
|
|
57
58
|
});
|
|
58
59
|
|
|
59
60
|
if (blocked) {
|
|
60
|
-
sourceContext.
|
|
61
|
+
sourceContext.securityException = [mode, ruleId];
|
|
61
62
|
throwSecurityException(sourceContext);
|
|
62
63
|
}
|
|
63
64
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { Logger } from '@contrast/logger';
|
|
17
17
|
import { Sources } from '@contrast/scopes';
|
|
18
18
|
import RequireHook from '@contrast/require-hook';
|
|
19
|
-
import { RulesConfig, Messages, ReqData, ProtectMessage,
|
|
19
|
+
import { RulesConfig, Messages, ReqData, ProtectMessage, ResultMap, ProtectRuleMode } from '@contrast/common';
|
|
20
20
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
21
21
|
import { Config } from '@contrast/config';
|
|
22
22
|
import * as http from 'node:http';
|
|
@@ -58,7 +58,10 @@ export interface ProtectRequestStore {
|
|
|
58
58
|
};
|
|
59
59
|
exclusions: any[]; // TODO
|
|
60
60
|
virtualPatches: any[]; // TODO
|
|
61
|
-
|
|
61
|
+
trackRequest: boolean;
|
|
62
|
+
securityException?: [mode: ProtectRuleMode, ruleId: string];
|
|
63
|
+
bodyType?: 'json' | 'urlencoded';
|
|
64
|
+
resultsMap: Partial<ResultMap>
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
export interface ConnectInputs {
|
package/lib/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const agentLib = require('@contrast/agent-lib');
|
|
19
|
-
const {
|
|
19
|
+
const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
20
20
|
|
|
21
21
|
module.exports = function(core) {
|
|
22
22
|
const protect = core.protect = {
|
|
@@ -35,7 +35,7 @@ module.exports = function(core) {
|
|
|
35
35
|
require('./error-handlers')(core);
|
|
36
36
|
|
|
37
37
|
protect.install = function() {
|
|
38
|
-
|
|
38
|
+
callChildComponentMethodsSync(protect, 'install');
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
return protect;
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
const {
|
|
19
19
|
BLOCKING_MODES,
|
|
20
|
-
|
|
20
|
+
traverseKeysAndValues,
|
|
21
|
+
traverseValues,
|
|
21
22
|
Rule,
|
|
22
23
|
isString,
|
|
23
24
|
ProtectRuleMode: { OFF },
|
|
@@ -199,7 +200,7 @@ module.exports = function(core) {
|
|
|
199
200
|
const resultsList = [];
|
|
200
201
|
const { UrlParameter } = agentLib.InputType;
|
|
201
202
|
|
|
202
|
-
|
|
203
|
+
traverseValues(urlParams, function(path, type, value) {
|
|
203
204
|
// url param names are not checked.
|
|
204
205
|
if (type !== 'Value') {
|
|
205
206
|
return;
|
|
@@ -306,7 +307,7 @@ module.exports = function(core) {
|
|
|
306
307
|
|
|
307
308
|
const block = commonObjectAnalyzer(sourceContext, parsedBody, inputTypes);
|
|
308
309
|
|
|
309
|
-
sourceContext.
|
|
310
|
+
sourceContext.bodyType = bodyType;
|
|
310
311
|
|
|
311
312
|
if (block) {
|
|
312
313
|
core.protect.throwSecurityException(sourceContext);
|
|
@@ -373,14 +374,14 @@ module.exports = function(core) {
|
|
|
373
374
|
const { name, uuid } = vpEvaluators.get('metadata');
|
|
374
375
|
|
|
375
376
|
if (vpEvaluators.size === 1 && uuid) {
|
|
376
|
-
if (!sourceContext.
|
|
377
|
-
sourceContext.
|
|
377
|
+
if (!sourceContext.resultsMap[ruleId]) {
|
|
378
|
+
sourceContext.resultsMap[ruleId] = [];
|
|
378
379
|
}
|
|
379
|
-
sourceContext.
|
|
380
|
+
sourceContext.resultsMap[ruleId].push({
|
|
380
381
|
name,
|
|
381
382
|
uuid
|
|
382
383
|
});
|
|
383
|
-
sourceContext.
|
|
384
|
+
sourceContext.securityException = ['block', ruleId];
|
|
384
385
|
core.protect.throwSecurityException(sourceContext);
|
|
385
386
|
}
|
|
386
387
|
}
|
|
@@ -412,11 +413,11 @@ module.exports = function(core) {
|
|
|
412
413
|
|
|
413
414
|
if (match) {
|
|
414
415
|
logger.info(match, 'Found a matching IP to an entry in ipDeny list');
|
|
415
|
-
if (!sourceContext.
|
|
416
|
-
sourceContext.
|
|
416
|
+
if (!sourceContext.resultsMap[ruleId]) {
|
|
417
|
+
sourceContext.resultsMap[ruleId] = [];
|
|
417
418
|
}
|
|
418
419
|
|
|
419
|
-
sourceContext.
|
|
420
|
+
sourceContext.resultsMap[ruleId].push({
|
|
420
421
|
ip: match.matchedIp,
|
|
421
422
|
uuid: match.uuid,
|
|
422
423
|
});
|
|
@@ -434,7 +435,7 @@ module.exports = function(core) {
|
|
|
434
435
|
inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) {
|
|
435
436
|
if (!config.protect.probe_analysis.enable || sourceContext.allowed) return;
|
|
436
437
|
|
|
437
|
-
const { resultsMap } = sourceContext
|
|
438
|
+
const { resultsMap } = sourceContext;
|
|
438
439
|
const probesRules = [Rule.CMD_INJECTION, Rule.PATH_TRAVERSAL, Rule.SQL_INJECTION, Rule.XXE];
|
|
439
440
|
const props = {};
|
|
440
441
|
|
|
@@ -444,11 +445,11 @@ module.exports = function(core) {
|
|
|
444
445
|
const {
|
|
445
446
|
ruleId,
|
|
446
447
|
blocked,
|
|
447
|
-
|
|
448
|
+
exploitMetadata,
|
|
448
449
|
value,
|
|
449
450
|
inputType
|
|
450
451
|
} = resultByRuleId;
|
|
451
|
-
if (blocked || !blocked &&
|
|
452
|
+
if (blocked || !blocked && exploitMetadata.length > 0 || !probesRules.some(rule => rule === ruleId)) return;
|
|
452
453
|
|
|
453
454
|
const { policy: { rulesMask } } = sourceContext;
|
|
454
455
|
|
|
@@ -514,19 +515,19 @@ module.exports = function(core) {
|
|
|
514
515
|
|
|
515
516
|
// it's possible to optimize this if qs (or a similar package) is not loaded
|
|
516
517
|
// or if none of the values of queryParams are objects. a quick '.includes()'
|
|
517
|
-
// could be used to determine that. if none are objects then
|
|
518
|
+
// could be used to determine that. if none are objects then traverseKeysAndValues()
|
|
518
519
|
// wouldn't be used, just a simple "for (const key in queryParams) {...}" to
|
|
519
520
|
// check each key and value associated with the key.
|
|
520
521
|
//
|
|
521
522
|
// otoh, it's a fair amount of additional logic and the gain is likely to be
|
|
522
523
|
// small, so it probably only makes sense to check if qs (or similar) is actually
|
|
523
|
-
// in use. a benchmark of "for (const key in queryParams) {...}" vs
|
|
524
|
+
// in use. a benchmark of "for (const key in queryParams) {...}" vs traverseKeysAndValues
|
|
524
525
|
// should be created to see if, and in what cases, it makes sense.
|
|
525
526
|
//
|
|
526
527
|
// another day.
|
|
527
528
|
|
|
528
529
|
/* eslint-disable-next-line complexity */
|
|
529
|
-
|
|
530
|
+
traverseKeysAndValues(object, function(path, type, value) {
|
|
530
531
|
let itemType;
|
|
531
532
|
let isMongoQueryType;
|
|
532
533
|
// this is a bit awkward now because nosql-injection-mongo is not integrated
|
|
@@ -708,10 +709,10 @@ function isResultExcluded(sourceContext, result) {
|
|
|
708
709
|
* @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
|
|
709
710
|
*/
|
|
710
711
|
function mergeFindings(sourceContext, newFindings) {
|
|
711
|
-
const {
|
|
712
|
+
const { policy, securityException, resultsMap } = sourceContext;
|
|
712
713
|
|
|
713
714
|
if (!newFindings.trackRequest) {
|
|
714
|
-
return
|
|
715
|
+
return securityException;
|
|
715
716
|
}
|
|
716
717
|
|
|
717
718
|
newFindings.resultsList = newFindings.resultsList.filter(
|
|
@@ -720,18 +721,18 @@ function mergeFindings(sourceContext, newFindings) {
|
|
|
720
721
|
|
|
721
722
|
normalizeFindings(policy, newFindings);
|
|
722
723
|
|
|
723
|
-
|
|
724
|
-
|
|
724
|
+
sourceContext.trackRequest = sourceContext.trackRequest || newFindings.trackRequest;
|
|
725
|
+
sourceContext.securityException = sourceContext.securityException || newFindings.securityException;
|
|
725
726
|
|
|
726
727
|
// merge them into a ruleId-indexed map (pojo)
|
|
727
728
|
for (const result of newFindings.resultsList) {
|
|
728
|
-
if (!
|
|
729
|
-
|
|
729
|
+
if (!resultsMap[result.ruleId]) {
|
|
730
|
+
resultsMap[result.ruleId] = [];
|
|
730
731
|
}
|
|
731
|
-
|
|
732
|
+
resultsMap[result.ruleId].push(result);
|
|
732
733
|
}
|
|
733
734
|
|
|
734
|
-
return
|
|
735
|
+
return sourceContext.securityException;
|
|
735
736
|
}
|
|
736
737
|
|
|
737
738
|
//
|
|
@@ -749,7 +750,7 @@ function normalizeFindings(policy, findings) {
|
|
|
749
750
|
r.blocked = false;
|
|
750
751
|
|
|
751
752
|
// sink analysis will add findings here
|
|
752
|
-
r.
|
|
753
|
+
r.exploitMetadata = [];
|
|
753
754
|
|
|
754
755
|
// apply exclusions here.
|
|
755
756
|
//
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const {
|
|
18
|
+
const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
19
|
|
|
20
20
|
module.exports = function(core) {
|
|
21
21
|
const inputAnalysis = core.protect.inputAnalysis = {};
|
|
@@ -47,7 +47,7 @@ module.exports = function(core) {
|
|
|
47
47
|
require('./ip-analysis')(core);
|
|
48
48
|
|
|
49
49
|
inputAnalysis.install = function() {
|
|
50
|
-
|
|
50
|
+
callChildComponentMethodsSync(inputAnalysis, 'install');
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
return inputAnalysis;
|
|
@@ -20,7 +20,8 @@ const {
|
|
|
20
20
|
ProtectRuleMode: { OFF },
|
|
21
21
|
BLOCKING_MODES,
|
|
22
22
|
isString,
|
|
23
|
-
|
|
23
|
+
traverseKeys,
|
|
24
|
+
traverseKeysAndValues,
|
|
24
25
|
} = require('@contrast/common');
|
|
25
26
|
|
|
26
27
|
module.exports = function(core) {
|
|
@@ -36,14 +37,14 @@ module.exports = function(core) {
|
|
|
36
37
|
function handleFindings(sourceContext, sinkContext, ruleId, result, findings) {
|
|
37
38
|
const { stacktraceData } = sinkContext;
|
|
38
39
|
captureStacktrace(sinkContext, stacktraceData);
|
|
39
|
-
result.
|
|
40
|
+
result.exploitMetadata.push({ sinkContext, findings });
|
|
40
41
|
|
|
41
42
|
const mode = sourceContext.policy[ruleId];
|
|
42
43
|
|
|
43
44
|
if (BLOCKING_MODES.includes(mode)) {
|
|
44
45
|
result.blocked = true;
|
|
45
46
|
const blockInfo = [mode, ruleId];
|
|
46
|
-
sourceContext.
|
|
47
|
+
sourceContext.securityException = blockInfo;
|
|
47
48
|
throwSecurityException(sourceContext);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
@@ -146,10 +147,13 @@ module.exports = function(core) {
|
|
|
146
147
|
|
|
147
148
|
for (const result of stringInjectionResults) {
|
|
148
149
|
if (typeof sinkContext.value === 'object') {
|
|
149
|
-
|
|
150
|
+
traverseKeysAndValues(sinkContext.value, function(path, type, value) {
|
|
150
151
|
if (type !== 'Key' && !agentLib.isMongoQueryType(value)) return;
|
|
151
152
|
|
|
152
153
|
stringFindings = handleStringValue(result, sinkContext.value[value], agentLib);
|
|
154
|
+
|
|
155
|
+
// halt traversal
|
|
156
|
+
return true;
|
|
153
157
|
});
|
|
154
158
|
} else if (typeof sinkContext.value === 'string') {
|
|
155
159
|
stringFindings = handleStringValue(result, sinkContext.value, agentLib);
|
|
@@ -158,11 +162,11 @@ module.exports = function(core) {
|
|
|
158
162
|
if (stringFindings) {
|
|
159
163
|
const nosqlInjectionResult = { ...result, ruleId, mappedId: ruleId };
|
|
160
164
|
|
|
161
|
-
const nosqlInjectionResults = sourceContext.
|
|
165
|
+
const nosqlInjectionResults = sourceContext.resultsMap[ruleId];
|
|
162
166
|
if (Array.isArray(nosqlInjectionResults)) {
|
|
163
167
|
nosqlInjectionResults.push(nosqlInjectionResult);
|
|
164
168
|
} else {
|
|
165
|
-
sourceContext.
|
|
169
|
+
sourceContext.resultsMap[ruleId] = [nosqlInjectionResult];
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
handleFindings(sourceContext, sinkContext, ruleId, nosqlInjectionResult, stringFindings);
|
|
@@ -231,7 +235,7 @@ module.exports = function(core) {
|
|
|
231
235
|
const findings = idx !== -1 ? { value: sinkContext.value } : null;
|
|
232
236
|
|
|
233
237
|
if (findings) {
|
|
234
|
-
result.
|
|
238
|
+
result.exploitMetadata.push({ sinkContext, findings });
|
|
235
239
|
}
|
|
236
240
|
}
|
|
237
241
|
};
|
|
@@ -250,18 +254,13 @@ function getResultsByRuleId(ruleId, context) {
|
|
|
250
254
|
if (context.policy[ruleId] === OFF) {
|
|
251
255
|
return;
|
|
252
256
|
}
|
|
253
|
-
return context.
|
|
257
|
+
return context.resultsMap[ruleId];
|
|
254
258
|
}
|
|
255
259
|
|
|
256
260
|
function handleObjectValue(result, object) {
|
|
257
|
-
if (typeof object !== 'object') {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
261
|
let findings = null;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
262
|
+
|
|
263
|
+
traverseKeys(object, function(path, type, value) {
|
|
265
264
|
// the result value is the key that was found
|
|
266
265
|
if (result.key === value) {
|
|
267
266
|
// does the object at this path equal the user input?
|
|
@@ -276,6 +275,9 @@ function handleObjectValue(result, object) {
|
|
|
276
275
|
const end = start + value.length;
|
|
277
276
|
const inputBoundaryIndex = 0;
|
|
278
277
|
findings = { start, end, boundaryOverrunIndex: start, inputBoundaryIndex };
|
|
278
|
+
|
|
279
|
+
// halt traversal
|
|
280
|
+
return true;
|
|
279
281
|
}
|
|
280
282
|
}
|
|
281
283
|
});
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const {
|
|
18
|
+
const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
19
|
|
|
20
20
|
module.exports = function(core) {
|
|
21
21
|
const inputTracing = core.protect.inputTracing = {};
|
|
@@ -38,7 +38,7 @@ module.exports = function(core) {
|
|
|
38
38
|
// TODO: NODE-2360 (oracledb)
|
|
39
39
|
|
|
40
40
|
inputTracing.install = function() {
|
|
41
|
-
|
|
41
|
+
callChildComponentMethodsSync(inputTracing, 'install');
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
return inputTracing;
|
|
@@ -23,10 +23,31 @@ module.exports = function(core) {
|
|
|
23
23
|
scopes: { instrumentation },
|
|
24
24
|
patcher,
|
|
25
25
|
depHooks,
|
|
26
|
-
protect,
|
|
27
|
-
protect: { inputTracing }
|
|
26
|
+
protect: { getSourceContext, inputTracing }
|
|
28
27
|
} = core;
|
|
29
28
|
|
|
29
|
+
function pre({ args, name, hooked, orig }) {
|
|
30
|
+
if (instrumentation.isLocked()) return;
|
|
31
|
+
|
|
32
|
+
const sourceContext = getSourceContext('child_process');
|
|
33
|
+
const value = args[0];
|
|
34
|
+
|
|
35
|
+
if (!sourceContext || !value || !isString(value)) return;
|
|
36
|
+
|
|
37
|
+
const sinkContext = {
|
|
38
|
+
name,
|
|
39
|
+
value,
|
|
40
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
inputTracing.handleCommandInjection(sourceContext, sinkContext);
|
|
44
|
+
core.protect.semanticAnalysis.handleCommandInjectionCommandBackdoors(sourceContext, sinkContext);
|
|
45
|
+
core.protect.semanticAnalysis.handleCmdInjectionSemanticChainedCommands(sourceContext, sinkContext);
|
|
46
|
+
core.protect.semanticAnalysis.handleCmdInjectionSemanticDangerous(sourceContext, sinkContext);
|
|
47
|
+
core.protect.semanticAnalysis.handlePathTraversalFileSecurityBypass(sourceContext, sinkContext);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
30
51
|
function install() {
|
|
31
52
|
depHooks.resolve({ name: 'child_process' }, cp => {
|
|
32
53
|
['exec', 'execSync'].forEach((method) => {
|
|
@@ -34,28 +55,7 @@ module.exports = function(core) {
|
|
|
34
55
|
patcher.patch(cp, method, {
|
|
35
56
|
name,
|
|
36
57
|
patchType,
|
|
37
|
-
pre
|
|
38
|
-
if (instrumentation.isLocked()) return;
|
|
39
|
-
|
|
40
|
-
const sourceContext = protect.getSourceContext('child_process');
|
|
41
|
-
const value = args[0];
|
|
42
|
-
|
|
43
|
-
if (!sourceContext || !value || !isString(value)) return;
|
|
44
|
-
|
|
45
|
-
const sinkContext = {
|
|
46
|
-
name,
|
|
47
|
-
value,
|
|
48
|
-
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
inputTracing.handleCommandInjection(sourceContext, sinkContext);
|
|
52
|
-
// To evade code duplication we are using these INPUT TRACING instrumentation
|
|
53
|
-
// to do the checks for SEMANTIC ANALYSIS too
|
|
54
|
-
core.protect.semanticAnalysis.handleCommandInjectionCommandBackdoors(sourceContext, sinkContext);
|
|
55
|
-
core.protect.semanticAnalysis.handleCmdInjectionSemanticChainedCommands(sourceContext, sinkContext);
|
|
56
|
-
core.protect.semanticAnalysis.handleCmdInjectionSemanticDangerous(sourceContext, sinkContext);
|
|
57
|
-
core.protect.semanticAnalysis.handlePathTraversalFileSecurityBypass(sourceContext, sinkContext);
|
|
58
|
-
}
|
|
58
|
+
pre
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
61
|
});
|
|
@@ -50,7 +50,7 @@ module.exports = function(core) {
|
|
|
50
50
|
value: fnBody,
|
|
51
51
|
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
52
52
|
};
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
inputTracing.ssjsInjection(sourceContext, sinkContext);
|
|
55
55
|
}
|
|
56
56
|
})
|
|
@@ -14,16 +14,14 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
|
-
|
|
18
|
-
const { patchType } = require('../constants');
|
|
17
|
+
const moduleName = 'mongodb';
|
|
19
18
|
const semver = require('semver');
|
|
19
|
+
const { patchType } = require('../constants');
|
|
20
20
|
|
|
21
21
|
module.exports = function (core) {
|
|
22
22
|
const {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
protect,
|
|
26
|
-
protect: { inputTracing },
|
|
23
|
+
protect: { getSourceContext, inputTracing },
|
|
24
|
+
instrumentation: { instrument }
|
|
27
25
|
} = core;
|
|
28
26
|
|
|
29
27
|
function getCursorQueryData(args, version) {
|
|
@@ -56,178 +54,163 @@ module.exports = function (core) {
|
|
|
56
54
|
return op.q;
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
name,
|
|
71
|
-
value,
|
|
72
|
-
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
73
|
-
};
|
|
74
|
-
inputTracing.nosqlInjectionMongo(sourceContext, sinkContext);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
57
|
+
function preHook(value, { name, hooked, orig }) {
|
|
58
|
+
const sourceContext = getSourceContext(name);
|
|
59
|
+
|
|
60
|
+
if (!sourceContext || !value) return;
|
|
61
|
+
|
|
62
|
+
const sinkContext = {
|
|
63
|
+
name,
|
|
64
|
+
value,
|
|
65
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
66
|
+
};
|
|
67
|
+
inputTracing.nosqlInjectionMongo(sourceContext, sinkContext);
|
|
77
68
|
}
|
|
78
69
|
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
70
|
+
function v4CollectionVal({ args, ...ctx }) {
|
|
71
|
+
const value = typeof args[0] == 'function' ? null : args[0];
|
|
72
|
+
preHook(value, ctx);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function v4DbVal({ args, ...ctx }) {
|
|
76
|
+
const value = args[0]?.filter;
|
|
77
|
+
preHook(value, ctx);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function v4CursorVal({ args, ...ctx }) {
|
|
81
|
+
const value = args[2];
|
|
82
|
+
preHook(value, ctx);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function v3CursorVal({ args, ...ctx }, version) {
|
|
86
|
+
const value = getCursorQueryData(args, version);
|
|
87
|
+
preHook(value, ctx);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function v3DbVal({ args, ...ctx }) {
|
|
91
|
+
const value = args[0];
|
|
92
|
+
preHook(value, ctx);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function v3TopologyVal({ args, ...ctx }) {
|
|
96
|
+
const ops = Array.isArray(args[1]) ? args[1] : [args[1]];
|
|
97
|
+
for (const op of ops) {
|
|
98
|
+
const value = op && getOpQueryData(op);
|
|
99
|
+
if (value) {
|
|
100
|
+
preHook(value, ctx);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
function install() {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
106
|
+
[
|
|
107
|
+
{
|
|
108
|
+
moduleName,
|
|
109
|
+
version: '>=4.0.0',
|
|
110
|
+
patchObjects: [
|
|
111
|
+
{
|
|
112
|
+
name: 'Collection.prototype',
|
|
113
|
+
methods: [
|
|
114
|
+
'updateOne',
|
|
115
|
+
'replaceOne',
|
|
116
|
+
'updateMany',
|
|
117
|
+
'deleteOne',
|
|
118
|
+
'deleteMany',
|
|
119
|
+
'findOneAndDelete',
|
|
120
|
+
'findOneAndReplace',
|
|
121
|
+
'findOneAndUpdate',
|
|
122
|
+
'countDocuments',
|
|
123
|
+
'count',
|
|
124
|
+
'distinct',
|
|
125
|
+
],
|
|
126
|
+
patchType,
|
|
127
|
+
preHookFn: v4CollectionVal
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'Db.prototype',
|
|
131
|
+
methods: ['command'],
|
|
132
|
+
patchType,
|
|
133
|
+
preHookFn: v4DbVal
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
},
|
|
122
137
|
{
|
|
123
|
-
|
|
138
|
+
moduleName,
|
|
139
|
+
version: '>=4.0.0',
|
|
140
|
+
file: 'lib/cursor/find_cursor',
|
|
141
|
+
patchObjects: [
|
|
142
|
+
{
|
|
143
|
+
methods: ['FindCursor'],
|
|
144
|
+
patchType,
|
|
145
|
+
preHookFn: v4CursorVal
|
|
146
|
+
}
|
|
147
|
+
]
|
|
124
148
|
},
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
{
|
|
150
|
+
moduleName,
|
|
151
|
+
version: '<4.0.0',
|
|
152
|
+
patchObjects: [
|
|
153
|
+
{
|
|
154
|
+
name: 'CoreServer.prototype',
|
|
155
|
+
methods: ['cursor'],
|
|
129
156
|
patchType,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
name,
|
|
138
|
-
value,
|
|
139
|
-
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
140
|
-
};
|
|
141
|
-
inputTracing.nosqlInjectionMongo(sourceContext, sinkContext);
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
patcher.patch(mongodb.Db.prototype, 'command', {
|
|
147
|
-
name: 'mongodb.Db.prototype.command',
|
|
148
|
-
patchType,
|
|
149
|
-
pre: ({ args, hooked, name, orig }) => {
|
|
150
|
-
const value = args[0]?.filter;
|
|
151
|
-
const sourceContext = protect.getSourceContext('mongodb.Collection.prototype.command');
|
|
152
|
-
|
|
153
|
-
if (!sourceContext || !value) return;
|
|
154
|
-
|
|
155
|
-
const sinkContext = {
|
|
156
|
-
name,
|
|
157
|
-
value,
|
|
158
|
-
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
159
|
-
};
|
|
160
|
-
inputTracing.nosqlInjectionMongo(sourceContext, sinkContext);
|
|
157
|
+
preHookFn: v3CursorVal
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'Db.prototype',
|
|
161
|
+
methods: ['eval'],
|
|
162
|
+
patchType,
|
|
163
|
+
preHookFn: v3DbVal
|
|
161
164
|
}
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
moduleName,
|
|
169
|
+
file: 'lib/topologies/topology_base.js',
|
|
170
|
+
version: '<4.0.0',
|
|
171
|
+
patchObjects: [
|
|
172
|
+
{
|
|
173
|
+
name: 'TopologyBase.prototype',
|
|
174
|
+
methods: ['update', 'remove'],
|
|
175
|
+
patchType,
|
|
176
|
+
preHookFn: v3TopologyVal
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'TopologyBase.prototype',
|
|
180
|
+
methods: ['command'],
|
|
181
|
+
patchType,
|
|
182
|
+
preHookFn: v3CursorVal
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
},
|
|
166
186
|
{
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
inputTracing.nosqlInjectionMongo(sourceContext, sinkContext);
|
|
187
|
+
moduleName,
|
|
188
|
+
file: 'lib/topologies/native_topology.js',
|
|
189
|
+
version: '<4.0.0',
|
|
190
|
+
patchObjects: [
|
|
191
|
+
{
|
|
192
|
+
name: 'NativeTopology.prototype',
|
|
193
|
+
patchName: 'prototype',
|
|
194
|
+
methods: ['update', 'remove'],
|
|
195
|
+
patchType,
|
|
196
|
+
preHookFn: v3TopologyVal
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'NativeTopology.prototype',
|
|
200
|
+
patchName: 'prototype',
|
|
201
|
+
methods: ['command'],
|
|
202
|
+
patchType,
|
|
203
|
+
preHookFn: v3CursorVal
|
|
187
204
|
}
|
|
188
|
-
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
depHooks.resolve({ name: 'mongodb', file: 'lib/cursor/find_cursor', version: '>=4.0.0' }, (cursor) => patcher.patch(cursor, 'FindCursor', {
|
|
192
|
-
name: 'mongodb.FindCursor',
|
|
193
|
-
patchType,
|
|
194
|
-
pre: ({ args, hooked, name, orig }) => {
|
|
195
|
-
const value = args[2];
|
|
196
|
-
const sourceContext = protect.getSourceContext('mongodb.FindCursor');
|
|
197
|
-
|
|
198
|
-
if (!sourceContext || !value) return;
|
|
199
|
-
|
|
200
|
-
const sinkContext = {
|
|
201
|
-
name,
|
|
202
|
-
value,
|
|
203
|
-
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
204
|
-
};
|
|
205
|
-
inputTracing.nosqlInjectionMongo(sourceContext, sinkContext);
|
|
205
|
+
]
|
|
206
206
|
}
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
file: 'lib/topologies/topology_base.js',
|
|
214
|
-
version: '<4.0.0'
|
|
215
|
-
}, (tpl, { version }) => {
|
|
216
|
-
mongoDBTopologiesMethods.forEach((method) => {
|
|
217
|
-
hookV3UpdateAndRemove(tpl.TopologyBase.prototype, method, `mongodb.TopologyBase.prototype.${method}`);
|
|
218
|
-
});
|
|
219
|
-
hookV3CommandAndCursor(tpl.TopologyBase.prototype, 'command', 'mongodb.TopologyBase.prototype.command', version);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
depHooks.resolve({
|
|
223
|
-
name: 'mongodb',
|
|
224
|
-
file: 'lib/topologies/native_topology.js',
|
|
225
|
-
version: '<4.0.0'
|
|
226
|
-
}, (NativeTopology, { version }) => {
|
|
227
|
-
mongoDBTopologiesMethods.forEach((method) => {
|
|
228
|
-
hookV3UpdateAndRemove(NativeTopology.prototype, method, `mongodb.NativeTopology.prototype.${method}`);
|
|
207
|
+
].forEach(({ moduleName, file, version, patchObjects }) => {
|
|
208
|
+
instrument({
|
|
209
|
+
moduleName,
|
|
210
|
+
file,
|
|
211
|
+
version,
|
|
212
|
+
patchObjects
|
|
229
213
|
});
|
|
230
|
-
hookV3CommandAndCursor(NativeTopology.prototype, 'command', 'mongodb.NativeTopology.prototype.command', version);
|
|
231
214
|
});
|
|
232
215
|
}
|
|
233
216
|
const mongodbInstr = (core.protect.inputTracing.mongodbInstrumentation = {
|
|
@@ -87,19 +87,12 @@ module.exports = function(core) {
|
|
|
87
87
|
exclusions: [],
|
|
88
88
|
virtualPatchesEvaluators: [],
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// successfully.
|
|
97
|
-
bodyType: undefined,
|
|
98
|
-
resultsMap: Object.create(null),
|
|
99
|
-
hardeningResultsMap: Object.create(null),
|
|
100
|
-
semanticResultsMap: Object.create(null),
|
|
101
|
-
serverFeaturesResultsMap: Object.create(null)
|
|
102
|
-
},
|
|
90
|
+
trackRequest: false,
|
|
91
|
+
securityException: undefined,
|
|
92
|
+
// bodyType is set to a body type if handlers.parseRawBody() parsed it
|
|
93
|
+
// successfully.
|
|
94
|
+
bodyType: undefined,
|
|
95
|
+
resultsMap: Object.create(null),
|
|
103
96
|
};
|
|
104
97
|
|
|
105
98
|
return protectStore;
|
|
@@ -20,7 +20,7 @@ const {
|
|
|
20
20
|
BLOCKING_MODES,
|
|
21
21
|
ProtectRuleMode: { OFF },
|
|
22
22
|
InputType,
|
|
23
|
-
|
|
23
|
+
traverseValues,
|
|
24
24
|
} = require('@contrast/common');
|
|
25
25
|
|
|
26
26
|
const {
|
|
@@ -45,17 +45,20 @@ module.exports = function(core) {
|
|
|
45
45
|
captureStacktrace(sinkContext, stacktraceData);
|
|
46
46
|
const result = {
|
|
47
47
|
blocked: false,
|
|
48
|
-
|
|
48
|
+
ruleId,
|
|
49
|
+
value,
|
|
50
|
+
mappedId: ruleId,
|
|
51
|
+
exploitMetadata: [{ command: value }],
|
|
49
52
|
sinkContext,
|
|
50
53
|
...finding
|
|
51
54
|
};
|
|
52
55
|
|
|
53
|
-
getRuleResults(sourceContext.
|
|
56
|
+
getRuleResults(sourceContext.resultsMap, ruleId).push(result);
|
|
54
57
|
|
|
55
58
|
if (BLOCKING_MODES.includes(mode)) {
|
|
56
59
|
result.blocked = true;
|
|
57
60
|
const blockInfo = [mode, ruleId];
|
|
58
|
-
sourceContext.
|
|
61
|
+
sourceContext.securityException = blockInfo;
|
|
59
62
|
throwSecurityException(sourceContext);
|
|
60
63
|
}
|
|
61
64
|
}
|
|
@@ -103,7 +106,7 @@ module.exports = function(core) {
|
|
|
103
106
|
|
|
104
107
|
if (agentLib.isDangerousPath(sinkContext.value, true)) {
|
|
105
108
|
handleResult(sourceContext, sinkContext, Rule.PATH_TRAVERSAL_SEMANTIC_FILE_SECURITY_BYPASS, mode, {
|
|
106
|
-
|
|
109
|
+
exploitMetadata: [{ path: sinkContext.value }]
|
|
107
110
|
});
|
|
108
111
|
}
|
|
109
112
|
};
|
|
@@ -115,7 +118,7 @@ module.exports = function(core) {
|
|
|
115
118
|
const findings = findExternalEntities(sinkContext.value);
|
|
116
119
|
if (findings.entities.length) {
|
|
117
120
|
handleResult(sourceContext, sinkContext, Rule.XXE, mode, {
|
|
118
|
-
findings,
|
|
121
|
+
exploitMetadata: [findings],
|
|
119
122
|
});
|
|
120
123
|
}
|
|
121
124
|
};
|
|
@@ -143,17 +146,15 @@ function findBackdoorInjection(sourceContext, command) {
|
|
|
143
146
|
[InputType.HEADER]: sourceContext.reqData.headers,
|
|
144
147
|
};
|
|
145
148
|
|
|
146
|
-
let found
|
|
149
|
+
let found;
|
|
147
150
|
for (const inputType in valuesOfInterest) {
|
|
151
|
+
if (found) break;
|
|
152
|
+
|
|
148
153
|
const values = valuesOfInterest[inputType];
|
|
149
154
|
|
|
150
155
|
if (values && Object.keys(values).length) {
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
!found &&
|
|
154
|
-
type === 'Value' &&
|
|
155
|
-
isBackdoorDetected(value, command)
|
|
156
|
-
) {
|
|
156
|
+
traverseValues(values, (path, type, value, obj) => {
|
|
157
|
+
if (isBackdoorDetected(value, command)) {
|
|
157
158
|
let key;
|
|
158
159
|
if (inputType === InputType.HEADER) {
|
|
159
160
|
key = obj[path[0] - 1];
|
|
@@ -167,6 +168,9 @@ function findBackdoorInjection(sourceContext, command) {
|
|
|
167
168
|
path: path.slice(0, -1),
|
|
168
169
|
value: command
|
|
169
170
|
};
|
|
171
|
+
|
|
172
|
+
// halt traversal
|
|
173
|
+
return true;
|
|
170
174
|
}
|
|
171
175
|
});
|
|
172
176
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const {
|
|
18
|
+
const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* SEMANTIC ANALYSIS is a STAGE of Protect.
|
|
@@ -37,11 +37,8 @@ module.exports = function(core) {
|
|
|
37
37
|
require('./install/libxmljs')(core);
|
|
38
38
|
|
|
39
39
|
semanticAnalysis.install = function() {
|
|
40
|
-
|
|
40
|
+
callChildComponentMethodsSync(semanticAnalysis, 'install');
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
// There is no `.install()` method as this STAGE does not introduce side effects on its own,
|
|
44
|
-
// it uses the instrumentation that's already in place for INPUT TRACING.
|
|
45
|
-
|
|
46
43
|
return semanticAnalysis;
|
|
47
44
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/protect",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Contrast service providing framework-agnostic Protect support",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@contrast/agent-lib": "^5.1.0",
|
|
21
|
-
"@contrast/common": "1.
|
|
22
|
-
"@contrast/core": "1.
|
|
23
|
-
"@contrast/esm-hooks": "1.
|
|
21
|
+
"@contrast/common": "1.2.0",
|
|
22
|
+
"@contrast/core": "1.8.0",
|
|
23
|
+
"@contrast/esm-hooks": "1.4.0",
|
|
24
24
|
"@contrast/scopes": "1.2.0",
|
|
25
25
|
"ipaddr.js": "^2.0.1",
|
|
26
26
|
"semver": "^7.3.7"
|
|
27
27
|
}
|
|
28
|
-
}
|
|
28
|
+
}
|