@contrast/protect 1.7.0 → 1.8.1
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/hardening/handlers.js +4 -2
- package/lib/hardening/install/node-serialize0.js +5 -5
- package/lib/input-analysis/handlers.js +156 -79
- package/lib/input-analysis/install/http.js +6 -7
- package/lib/input-analysis/virtual-patches.js +1 -1
- package/lib/input-tracing/handlers/index.js +10 -1
- package/lib/input-tracing/install/child-process.js +5 -5
- package/lib/input-tracing/install/eval.js +5 -5
- package/lib/input-tracing/install/fs.js +5 -5
- package/lib/input-tracing/install/function.js +6 -5
- package/lib/input-tracing/install/http.js +5 -5
- package/lib/input-tracing/install/mongodb.js +30 -25
- package/lib/input-tracing/install/mysql.js +5 -5
- package/lib/input-tracing/install/postgres.js +5 -5
- package/lib/input-tracing/install/sequelize.js +6 -5
- package/lib/input-tracing/install/sqlite3.js +5 -5
- package/lib/input-tracing/install/vm.js +25 -21
- package/lib/make-source-context.js +15 -13
- package/lib/policy.js +224 -10
- package/lib/semantic-analysis/handlers.js +4 -2
- package/lib/semantic-analysis/install/libxmljs.js +5 -5
- package/package.json +4 -4
|
@@ -24,7 +24,8 @@ module.exports = function(core) {
|
|
|
24
24
|
protect: {
|
|
25
25
|
hardening,
|
|
26
26
|
throwSecurityException,
|
|
27
|
-
}
|
|
27
|
+
},
|
|
28
|
+
captureStacktrace,
|
|
28
29
|
} = core;
|
|
29
30
|
|
|
30
31
|
function getResults(sourceContext, ruleId) {
|
|
@@ -38,7 +39,7 @@ module.exports = function(core) {
|
|
|
38
39
|
hardening.handleUntrustedDeserialization = function(sourceContext, sinkContext) {
|
|
39
40
|
const ruleId = 'untrusted-deserialization';
|
|
40
41
|
const mode = sourceContext.policy[ruleId];
|
|
41
|
-
const { name, value } = sinkContext;
|
|
42
|
+
const { name, value, stacktraceData } = sinkContext;
|
|
42
43
|
|
|
43
44
|
if (mode === 'off') return;
|
|
44
45
|
|
|
@@ -48,6 +49,7 @@ module.exports = function(core) {
|
|
|
48
49
|
const blocked = BLOCKING_MODES.includes(mode);
|
|
49
50
|
const results = getResults(sourceContext, ruleId);
|
|
50
51
|
|
|
52
|
+
captureStacktrace(sinkContext, stacktraceData);
|
|
51
53
|
results.push({
|
|
52
54
|
blocked,
|
|
53
55
|
findings: { deserializer: name, command: false },
|
|
@@ -21,7 +21,6 @@ module.exports = function(core) {
|
|
|
21
21
|
const {
|
|
22
22
|
depHooks,
|
|
23
23
|
patcher,
|
|
24
|
-
captureStacktrace,
|
|
25
24
|
protect,
|
|
26
25
|
protect: {
|
|
27
26
|
hardening
|
|
@@ -43,10 +42,11 @@ module.exports = function(core) {
|
|
|
43
42
|
|
|
44
43
|
if (!sourceContext || !value) return;
|
|
45
44
|
|
|
46
|
-
const sinkContext =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
const sinkContext = {
|
|
46
|
+
name: `${name}.${method}`,
|
|
47
|
+
value,
|
|
48
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
49
|
+
};
|
|
50
50
|
hardening.handleUntrustedDeserialization(sourceContext, sinkContext);
|
|
51
51
|
},
|
|
52
52
|
});
|
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
BLOCKING_MODES,
|
|
20
|
+
simpleTraverse,
|
|
21
|
+
Rule,
|
|
22
|
+
isString,
|
|
23
|
+
ProtectRuleMode: { OFF },
|
|
24
|
+
} = require('@contrast/common');
|
|
19
25
|
const address = require('ipaddr.js');
|
|
20
26
|
|
|
21
27
|
//
|
|
@@ -57,6 +63,14 @@ module.exports = function(core) {
|
|
|
57
63
|
config,
|
|
58
64
|
} = core;
|
|
59
65
|
|
|
66
|
+
const jsonInputTypes = {
|
|
67
|
+
keyType: agentLib.InputType.JsonKey, inputType: agentLib.InputType.JsonValue
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const parameterInputTypes = {
|
|
71
|
+
keyType: agentLib.InputType.ParameterKey, inputType: agentLib.InputType.ParameterValue
|
|
72
|
+
};
|
|
73
|
+
|
|
60
74
|
// all handlers will be invoked with two arguments:
|
|
61
75
|
// 1) sourceContext object containing:
|
|
62
76
|
// - reqData, the abstract request object containing only what is needed
|
|
@@ -112,8 +126,6 @@ module.exports = function(core) {
|
|
|
112
126
|
* @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
|
|
113
127
|
*/
|
|
114
128
|
inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
|
|
115
|
-
if (!sourceContext || sourceContext.allowed) return;
|
|
116
|
-
|
|
117
129
|
const { policy: { rulesMask } } = sourceContext;
|
|
118
130
|
|
|
119
131
|
inputAnalysis.handleVirtualPatches(sourceContext, { URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers });
|
|
@@ -122,84 +134,13 @@ module.exports = function(core) {
|
|
|
122
134
|
let block = undefined;
|
|
123
135
|
if (rulesMask !== 0) {
|
|
124
136
|
const findings = agentLib.scoreRequestConnect(rulesMask, connectInputs, preferWW);
|
|
137
|
+
|
|
125
138
|
block = mergeFindings(sourceContext, findings);
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
return block;
|
|
129
142
|
};
|
|
130
143
|
|
|
131
|
-
/**
|
|
132
|
-
* handleRequestEnd()
|
|
133
|
-
*
|
|
134
|
-
* Invoked when the request is complete.
|
|
135
|
-
*
|
|
136
|
-
* @param {Object} sourceContext
|
|
137
|
-
*/
|
|
138
|
-
inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) {
|
|
139
|
-
if (!config.protect.probe_analysis.enable) return;
|
|
140
|
-
|
|
141
|
-
const { resultsMap } = sourceContext.findings;
|
|
142
|
-
const probesRules = [Rule.CMD_INJECTION, Rule.PATH_TRAVERSAL, Rule.SQL_INJECTION, Rule.XXE];
|
|
143
|
-
const props = {};
|
|
144
|
-
|
|
145
|
-
// Detecting probes
|
|
146
|
-
Object.values(resultsMap).forEach(resultsByRuleId => {
|
|
147
|
-
resultsByRuleId.forEach((resultByRuleId) => {
|
|
148
|
-
const {
|
|
149
|
-
ruleId,
|
|
150
|
-
blocked,
|
|
151
|
-
details,
|
|
152
|
-
value,
|
|
153
|
-
inputType
|
|
154
|
-
} = resultByRuleId;
|
|
155
|
-
if (blocked || !blocked && details.length > 0 || !probesRules.some(rule => rule === ruleId)) return;
|
|
156
|
-
|
|
157
|
-
const { policy: { rulesMask } } = sourceContext;
|
|
158
|
-
|
|
159
|
-
const results = (agentLib.scoreAtom(
|
|
160
|
-
rulesMask,
|
|
161
|
-
value,
|
|
162
|
-
agentLib.InputType[inputType],
|
|
163
|
-
{
|
|
164
|
-
preferWorthWatching: false
|
|
165
|
-
}
|
|
166
|
-
) || []).filter(({ score }) => score >= 90);
|
|
167
|
-
|
|
168
|
-
if (!results.length) return;
|
|
169
|
-
|
|
170
|
-
results.forEach(result => {
|
|
171
|
-
const isAlreadyBlocked = (resultsMap[result.ruleId] || []).some(element =>
|
|
172
|
-
element.blocked && element.inputType === inputType && element.value === value
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
if (isAlreadyBlocked) return;
|
|
176
|
-
|
|
177
|
-
const probe = Object.assign({}, resultByRuleId, result, {
|
|
178
|
-
mappedId: result.ruleId
|
|
179
|
-
});
|
|
180
|
-
const key = [probe.ruleId, probe.inputType, ...probe.path, probe.value].join('|');
|
|
181
|
-
props[key] = probe;
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
Object.values(props).forEach(prop => {
|
|
187
|
-
if (!resultsMap[prop.ruleId]) {
|
|
188
|
-
resultsMap[prop.ruleId] = [];
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
resultsMap[prop.ruleId].push(prop);
|
|
192
|
-
});
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const jsonInputTypes = {
|
|
196
|
-
keyType: agentLib.InputType.JsonKey, inputType: agentLib.InputType.JsonValue
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const parameterInputTypes = {
|
|
200
|
-
keyType: agentLib.InputType.ParameterKey, inputType: agentLib.InputType.ParameterValue
|
|
201
|
-
};
|
|
202
|
-
|
|
203
144
|
/**
|
|
204
145
|
* handleQueryParams()
|
|
205
146
|
*
|
|
@@ -225,7 +166,6 @@ module.exports = function(core) {
|
|
|
225
166
|
return;
|
|
226
167
|
}
|
|
227
168
|
|
|
228
|
-
|
|
229
169
|
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: queryParams });
|
|
230
170
|
|
|
231
171
|
const block = commonObjectAnalyzer(sourceContext, queryParams, parameterInputTypes);
|
|
@@ -264,10 +204,13 @@ module.exports = function(core) {
|
|
|
264
204
|
if (type !== 'Value') {
|
|
265
205
|
return;
|
|
266
206
|
}
|
|
207
|
+
|
|
267
208
|
const items = agentLib.scoreAtom(rulesMask, value, UrlParameter, preferWW);
|
|
209
|
+
|
|
268
210
|
if (!items) {
|
|
269
211
|
return;
|
|
270
212
|
}
|
|
213
|
+
|
|
271
214
|
for (const item of items) {
|
|
272
215
|
resultsList.push({
|
|
273
216
|
ruleId: item.ruleId,
|
|
@@ -312,14 +255,15 @@ module.exports = function(core) {
|
|
|
312
255
|
if (sourceContext.analyzedCookies) return;
|
|
313
256
|
sourceContext.analyzedCookies = true;
|
|
314
257
|
|
|
258
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { HEADERS: cookies });
|
|
259
|
+
|
|
260
|
+
const { policy: { rulesMask } } = sourceContext;
|
|
261
|
+
|
|
315
262
|
const cookiesArr = Object.entries(cookies).reduce((acc, [key, value]) => {
|
|
316
263
|
acc.push(key, value);
|
|
317
264
|
return acc;
|
|
318
265
|
}, []);
|
|
319
266
|
|
|
320
|
-
inputAnalysis.handleVirtualPatches(sourceContext, { HEADERS: cookies });
|
|
321
|
-
|
|
322
|
-
const { policy: { rulesMask } } = sourceContext;
|
|
323
267
|
const cookieFindings = agentLib.scoreRequestConnect(rulesMask, { cookies: cookiesArr }, preferWW);
|
|
324
268
|
|
|
325
269
|
const block = mergeFindings(sourceContext, cookieFindings);
|
|
@@ -340,6 +284,7 @@ module.exports = function(core) {
|
|
|
340
284
|
*/
|
|
341
285
|
inputAnalysis.handleParsedBody = function(sourceContext, parsedBody) {
|
|
342
286
|
if (sourceContext.analyzedBody) return;
|
|
287
|
+
|
|
343
288
|
sourceContext.analyzedBody = true;
|
|
344
289
|
|
|
345
290
|
if (typeof parsedBody !== 'object') {
|
|
@@ -358,6 +303,7 @@ module.exports = function(core) {
|
|
|
358
303
|
bodyType = 'urlencoded';
|
|
359
304
|
inputTypes = parameterInputTypes;
|
|
360
305
|
}
|
|
306
|
+
|
|
361
307
|
const block = commonObjectAnalyzer(sourceContext, parsedBody, inputTypes);
|
|
362
308
|
|
|
363
309
|
sourceContext.findings.bodyType = bodyType;
|
|
@@ -478,6 +424,70 @@ module.exports = function(core) {
|
|
|
478
424
|
}
|
|
479
425
|
};
|
|
480
426
|
|
|
427
|
+
/**
|
|
428
|
+
* handleRequestEnd()
|
|
429
|
+
*
|
|
430
|
+
* Invoked when the request is complete.
|
|
431
|
+
*
|
|
432
|
+
* @param {Object} sourceContext
|
|
433
|
+
*/
|
|
434
|
+
inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) {
|
|
435
|
+
if (!config.protect.probe_analysis.enable || sourceContext.allowed) return;
|
|
436
|
+
|
|
437
|
+
const { resultsMap } = sourceContext.findings;
|
|
438
|
+
const probesRules = [Rule.CMD_INJECTION, Rule.PATH_TRAVERSAL, Rule.SQL_INJECTION, Rule.XXE];
|
|
439
|
+
const props = {};
|
|
440
|
+
|
|
441
|
+
// Detecting probes
|
|
442
|
+
Object.values(resultsMap).forEach(resultsByRuleId => {
|
|
443
|
+
resultsByRuleId.forEach((resultByRuleId) => {
|
|
444
|
+
const {
|
|
445
|
+
ruleId,
|
|
446
|
+
blocked,
|
|
447
|
+
details,
|
|
448
|
+
value,
|
|
449
|
+
inputType
|
|
450
|
+
} = resultByRuleId;
|
|
451
|
+
if (blocked || !blocked && details.length > 0 || !probesRules.some(rule => rule === ruleId)) return;
|
|
452
|
+
|
|
453
|
+
const { policy: { rulesMask } } = sourceContext;
|
|
454
|
+
|
|
455
|
+
const results = (agentLib.scoreAtom(
|
|
456
|
+
rulesMask,
|
|
457
|
+
value,
|
|
458
|
+
agentLib.InputType[inputType],
|
|
459
|
+
{
|
|
460
|
+
preferWorthWatching: false
|
|
461
|
+
}
|
|
462
|
+
) || []).filter(({ score }) => score >= 90);
|
|
463
|
+
|
|
464
|
+
if (!results.length) return;
|
|
465
|
+
|
|
466
|
+
results.forEach(result => {
|
|
467
|
+
const isAlreadyBlocked = (resultsMap[result.ruleId] || []).some(element =>
|
|
468
|
+
element.blocked && element.inputType === inputType && element.value === value
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
if (isAlreadyBlocked) return;
|
|
472
|
+
|
|
473
|
+
const probe = Object.assign({}, resultByRuleId, result, {
|
|
474
|
+
mappedId: result.ruleId
|
|
475
|
+
});
|
|
476
|
+
const key = [probe.ruleId, probe.inputType, ...probe.path, probe.value].join('|');
|
|
477
|
+
props[key] = probe;
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
Object.values(props).forEach(prop => {
|
|
483
|
+
if (!resultsMap[prop.ruleId]) {
|
|
484
|
+
resultsMap[prop.ruleId] = [];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
resultsMap[prop.ruleId].push(prop);
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
|
|
481
491
|
/**
|
|
482
492
|
* commonObjectAnalyzer() walks an object supplied by the end-user and checks
|
|
483
493
|
* it for vulnerabilities.
|
|
@@ -532,7 +542,9 @@ module.exports = function(core) {
|
|
|
532
542
|
} else {
|
|
533
543
|
itemType = inputType;
|
|
534
544
|
}
|
|
545
|
+
|
|
535
546
|
let items = agentLib.scoreAtom(rulesMask, value, itemType, preferWW);
|
|
547
|
+
|
|
536
548
|
if (!items && !isMongoQueryType) {
|
|
537
549
|
return;
|
|
538
550
|
}
|
|
@@ -628,6 +640,66 @@ module.exports = function(core) {
|
|
|
628
640
|
}
|
|
629
641
|
};
|
|
630
642
|
|
|
643
|
+
/**
|
|
644
|
+
* Reads the source context's policy and compares to result item to check whether to ignore it.
|
|
645
|
+
* @param {ProtectMessage} sourceContext
|
|
646
|
+
* @param {Result} result
|
|
647
|
+
* @returns {boolean} whether result should be excluded
|
|
648
|
+
*/
|
|
649
|
+
function isResultExcluded(sourceContext, result) {
|
|
650
|
+
const { policy: { exclusions } } = sourceContext;
|
|
651
|
+
const { ruleId, path, inputType, value } = result;
|
|
652
|
+
const inputName = path ? path[path.length - 1] : null;
|
|
653
|
+
|
|
654
|
+
let checkCookiesInHeader = false;
|
|
655
|
+
let inputExclusions;
|
|
656
|
+
switch (inputType) {
|
|
657
|
+
case 'JsonKey':
|
|
658
|
+
case 'JsonValue':
|
|
659
|
+
case 'MultipartName': {
|
|
660
|
+
return exclusions.ignoreBody || exclusions.bodyPolicy?.[ruleId] === OFF;
|
|
661
|
+
}
|
|
662
|
+
case 'ParameterKey':
|
|
663
|
+
case 'ParameterValue': {
|
|
664
|
+
const qsExcluded = exclusions.ignoreQuerystring || exclusions.querystringPolicy?.[ruleId] === OFF;
|
|
665
|
+
if (qsExcluded) return true;
|
|
666
|
+
inputExclusions = exclusions.parameter;
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
case 'CookieValue': {
|
|
670
|
+
inputExclusions = exclusions.cookie;
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
case 'HeaderKey':
|
|
674
|
+
case 'HeaderValue': {
|
|
675
|
+
if (path?.[0]?.toLowerCase() === 'cookie') {
|
|
676
|
+
inputExclusions = exclusions.cookie;
|
|
677
|
+
checkCookiesInHeader = true;
|
|
678
|
+
} else {
|
|
679
|
+
inputExclusions = exclusions.header;
|
|
680
|
+
}
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (!inputName || !inputExclusions) return false;
|
|
686
|
+
|
|
687
|
+
for (const excl of inputExclusions) {
|
|
688
|
+
let nameCheck = false;
|
|
689
|
+
if (checkCookiesInHeader) {
|
|
690
|
+
nameCheck = excl.checkCookiesInHeader(value);
|
|
691
|
+
} else {
|
|
692
|
+
nameCheck = excl.matchesInputName(inputName);
|
|
693
|
+
}
|
|
694
|
+
if (!nameCheck) continue;
|
|
695
|
+
if (!excl.policy || excl.policy[ruleId] === OFF) {
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
|
|
631
703
|
/**
|
|
632
704
|
* merge new findings into the existing findings
|
|
633
705
|
*
|
|
@@ -641,6 +713,11 @@ function mergeFindings(sourceContext, newFindings) {
|
|
|
641
713
|
if (!newFindings.trackRequest) {
|
|
642
714
|
return findings.securityException;
|
|
643
715
|
}
|
|
716
|
+
|
|
717
|
+
newFindings.resultsList = newFindings.resultsList.filter(
|
|
718
|
+
(result) => !isResultExcluded(sourceContext, result)
|
|
719
|
+
);
|
|
720
|
+
|
|
644
721
|
normalizeFindings(policy, newFindings);
|
|
645
722
|
|
|
646
723
|
findings.trackRequest = findings.trackRequest || newFindings.trackRequest;
|
|
@@ -176,8 +176,13 @@ class HttpInstrumentation {
|
|
|
176
176
|
setImmediate(() => method.call(instance, ...args));
|
|
177
177
|
return;
|
|
178
178
|
}
|
|
179
|
-
|
|
180
179
|
store.protect = this.makeSourceContext(req, res);
|
|
180
|
+
|
|
181
|
+
if (store.protect.allowed) {
|
|
182
|
+
setImmediate(() => method.call(instance, ...args));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
181
186
|
const { reqData } = store.protect;
|
|
182
187
|
|
|
183
188
|
res.on('finish', () => {
|
|
@@ -209,12 +214,6 @@ class HttpInstrumentation {
|
|
|
209
214
|
method: reqData.method,
|
|
210
215
|
};
|
|
211
216
|
|
|
212
|
-
// only add queries if it's known that 'qs' or equivalent won't be used.
|
|
213
|
-
/* c8 ignore next 3 */
|
|
214
|
-
if (reqData.standardUrlParsing) {
|
|
215
|
-
connectInputs.queries = reqData.queries;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
217
|
if (inputAnalysis.virtualPatchesEvaluators?.length) {
|
|
219
218
|
store.protect.virtualPatchesEvaluators.push(...inputAnalysis.virtualPatchesEvaluators.map((e) => new Map(e)));
|
|
220
219
|
}
|
|
@@ -26,7 +26,7 @@ module.exports = (core) => {
|
|
|
26
26
|
const virtualPatchesEvaluators = inputAnalysis.virtualPatchesEvaluators = [];
|
|
27
27
|
|
|
28
28
|
messages.on(Event.SERVER_SETTINGS_UPDATE, (serverUpdate) => {
|
|
29
|
-
const virtualPatches = serverUpdate.settings?.defend
|
|
29
|
+
const virtualPatches = serverUpdate.settings?.defend?.virtualPatches;
|
|
30
30
|
if (virtualPatches) {
|
|
31
31
|
buildVPEvaluators(virtualPatches, virtualPatchesEvaluators);
|
|
32
32
|
}
|
|
@@ -24,9 +24,18 @@ const {
|
|
|
24
24
|
} = require('@contrast/common');
|
|
25
25
|
|
|
26
26
|
module.exports = function(core) {
|
|
27
|
-
const {
|
|
27
|
+
const {
|
|
28
|
+
protect: {
|
|
29
|
+
agentLib,
|
|
30
|
+
inputTracing,
|
|
31
|
+
throwSecurityException
|
|
32
|
+
},
|
|
33
|
+
captureStacktrace,
|
|
34
|
+
} = core;
|
|
28
35
|
|
|
29
36
|
function handleFindings(sourceContext, sinkContext, ruleId, result, findings) {
|
|
37
|
+
const { stacktraceData } = sinkContext;
|
|
38
|
+
captureStacktrace(sinkContext, stacktraceData);
|
|
30
39
|
result.details.push({ sinkContext, findings });
|
|
31
40
|
|
|
32
41
|
const mode = sourceContext.policy[ruleId];
|
|
@@ -23,7 +23,6 @@ module.exports = function(core) {
|
|
|
23
23
|
scopes: { instrumentation },
|
|
24
24
|
patcher,
|
|
25
25
|
depHooks,
|
|
26
|
-
captureStacktrace,
|
|
27
26
|
protect,
|
|
28
27
|
protect: { inputTracing }
|
|
29
28
|
} = core;
|
|
@@ -43,10 +42,11 @@ module.exports = function(core) {
|
|
|
43
42
|
|
|
44
43
|
if (!sourceContext || !value || !isString(value)) return;
|
|
45
44
|
|
|
46
|
-
const sinkContext =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
const sinkContext = {
|
|
46
|
+
name,
|
|
47
|
+
value,
|
|
48
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
49
|
+
};
|
|
50
50
|
|
|
51
51
|
inputTracing.handleCommandInjection(sourceContext, sinkContext);
|
|
52
52
|
// To evade code duplication we are using these INPUT TRACING instrumentation
|
|
@@ -23,7 +23,6 @@ module.exports = function(core) {
|
|
|
23
23
|
logger,
|
|
24
24
|
scopes: { instrumentation },
|
|
25
25
|
patcher,
|
|
26
|
-
captureStacktrace,
|
|
27
26
|
protect,
|
|
28
27
|
protect: { inputTracing }
|
|
29
28
|
} = core;
|
|
@@ -45,10 +44,11 @@ module.exports = function(core) {
|
|
|
45
44
|
|
|
46
45
|
if (!sourceContext || !value || !isString(value)) return;
|
|
47
46
|
|
|
48
|
-
const sinkContext =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
const sinkContext = {
|
|
48
|
+
name: 'eval',
|
|
49
|
+
value,
|
|
50
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
51
|
+
};
|
|
52
52
|
inputTracing.ssjsInjection(sourceContext, sinkContext);
|
|
53
53
|
}
|
|
54
54
|
});
|
|
@@ -64,7 +64,6 @@ module.exports = function(core) {
|
|
|
64
64
|
scopes: { instrumentation },
|
|
65
65
|
patcher,
|
|
66
66
|
depHooks,
|
|
67
|
-
captureStacktrace,
|
|
68
67
|
protect,
|
|
69
68
|
protect: { inputTracing }
|
|
70
69
|
} = core;
|
|
@@ -101,10 +100,11 @@ module.exports = function(core) {
|
|
|
101
100
|
// don't need to necessarily need to lock it here - there are no
|
|
102
101
|
// lower-level calls that we instrument
|
|
103
102
|
for (const value of values) {
|
|
104
|
-
const sinkContext =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
const sinkContext = {
|
|
104
|
+
name,
|
|
105
|
+
value,
|
|
106
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
107
|
+
};
|
|
108
108
|
inputTracing.handlePathTraversal(sourceContext, sinkContext);
|
|
109
109
|
core.protect.semanticAnalysis.handlePathTraversalFileSecurityBypass(sourceContext, sinkContext);
|
|
110
110
|
}
|
|
@@ -23,7 +23,6 @@ module.exports = function(core) {
|
|
|
23
23
|
logger,
|
|
24
24
|
scopes: { instrumentation },
|
|
25
25
|
patcher,
|
|
26
|
-
captureStacktrace,
|
|
27
26
|
protect,
|
|
28
27
|
protect: { inputTracing }
|
|
29
28
|
} = core;
|
|
@@ -46,10 +45,12 @@ module.exports = function(core) {
|
|
|
46
45
|
|
|
47
46
|
if (!sourceContext || !fnBody || !isString(fnBody)) return;
|
|
48
47
|
|
|
49
|
-
const sinkContext =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
const sinkContext = {
|
|
49
|
+
name: 'Function',
|
|
50
|
+
value: fnBody,
|
|
51
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
52
|
+
};
|
|
53
|
+
console.log({ sinkContext: sinkContext.stacktraceData });
|
|
53
54
|
inputTracing.ssjsInjection(sourceContext, sinkContext);
|
|
54
55
|
}
|
|
55
56
|
})
|
|
@@ -22,7 +22,6 @@ module.exports = function(core) {
|
|
|
22
22
|
scopes: { instrumentation },
|
|
23
23
|
patcher,
|
|
24
24
|
depHooks,
|
|
25
|
-
captureStacktrace,
|
|
26
25
|
protect,
|
|
27
26
|
protect: { inputTracing }
|
|
28
27
|
} = core;
|
|
@@ -44,10 +43,11 @@ module.exports = function(core) {
|
|
|
44
43
|
const value = data.args[0]?.toString();
|
|
45
44
|
if (!value) return;
|
|
46
45
|
|
|
47
|
-
const sinkContext =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
const sinkContext = {
|
|
47
|
+
name,
|
|
48
|
+
value,
|
|
49
|
+
stacktraceData: { constructorOpt: data.hooked },
|
|
50
|
+
};
|
|
51
51
|
inputTracing.handleReflectedXss(sourceContext, sinkContext);
|
|
52
52
|
}
|
|
53
53
|
});
|
|
@@ -22,7 +22,6 @@ module.exports = function (core) {
|
|
|
22
22
|
const {
|
|
23
23
|
depHooks,
|
|
24
24
|
patcher,
|
|
25
|
-
captureStacktrace,
|
|
26
25
|
protect,
|
|
27
26
|
protect: { inputTracing },
|
|
28
27
|
} = core;
|
|
@@ -67,10 +66,11 @@ module.exports = function (core) {
|
|
|
67
66
|
|
|
68
67
|
if (!sourceContext || !value) return;
|
|
69
68
|
|
|
70
|
-
const sinkContext =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
const sinkContext = {
|
|
70
|
+
name,
|
|
71
|
+
value,
|
|
72
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
73
|
+
};
|
|
74
74
|
inputTracing.nosqlInjectionMongo(sourceContext, sinkContext);
|
|
75
75
|
}
|
|
76
76
|
});
|
|
@@ -91,10 +91,11 @@ module.exports = function (core) {
|
|
|
91
91
|
for (const op of ops) {
|
|
92
92
|
const value = op && getOpQueryData(op);
|
|
93
93
|
if (value) {
|
|
94
|
-
const sinkContext =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
const sinkContext = {
|
|
95
|
+
name,
|
|
96
|
+
value,
|
|
97
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
98
|
+
};
|
|
98
99
|
inputTracing.nosqlInjectionMongo(sourceContext, sinkContext);
|
|
99
100
|
}
|
|
100
101
|
}
|
|
@@ -132,10 +133,11 @@ module.exports = function (core) {
|
|
|
132
133
|
|
|
133
134
|
if (!sourceContext || !value) return;
|
|
134
135
|
|
|
135
|
-
const sinkContext =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
const sinkContext = {
|
|
137
|
+
name,
|
|
138
|
+
value,
|
|
139
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
140
|
+
};
|
|
139
141
|
inputTracing.nosqlInjectionMongo(sourceContext, sinkContext);
|
|
140
142
|
},
|
|
141
143
|
});
|
|
@@ -150,10 +152,11 @@ module.exports = function (core) {
|
|
|
150
152
|
|
|
151
153
|
if (!sourceContext || !value) return;
|
|
152
154
|
|
|
153
|
-
const sinkContext =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
const sinkContext = {
|
|
156
|
+
name,
|
|
157
|
+
value,
|
|
158
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
159
|
+
};
|
|
157
160
|
inputTracing.nosqlInjectionMongo(sourceContext, sinkContext);
|
|
158
161
|
}
|
|
159
162
|
});
|
|
@@ -174,10 +177,11 @@ module.exports = function (core) {
|
|
|
174
177
|
|
|
175
178
|
if (!sourceContext || !value) return;
|
|
176
179
|
|
|
177
|
-
const sinkContext =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
180
|
+
const sinkContext = {
|
|
181
|
+
name,
|
|
182
|
+
value,
|
|
183
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
184
|
+
};
|
|
181
185
|
|
|
182
186
|
inputTracing.nosqlInjectionMongo(sourceContext, sinkContext);
|
|
183
187
|
}
|
|
@@ -193,10 +197,11 @@ module.exports = function (core) {
|
|
|
193
197
|
|
|
194
198
|
if (!sourceContext || !value) return;
|
|
195
199
|
|
|
196
|
-
const sinkContext =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
const sinkContext = {
|
|
201
|
+
name,
|
|
202
|
+
value,
|
|
203
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
204
|
+
};
|
|
200
205
|
inputTracing.nosqlInjectionMongo(sourceContext, sinkContext);
|
|
201
206
|
}
|
|
202
207
|
}));
|
|
@@ -22,7 +22,6 @@ module.exports = function(core) {
|
|
|
22
22
|
const {
|
|
23
23
|
depHooks,
|
|
24
24
|
patcher,
|
|
25
|
-
captureStacktrace,
|
|
26
25
|
protect,
|
|
27
26
|
protect: { inputTracing }
|
|
28
27
|
} = core;
|
|
@@ -56,10 +55,11 @@ module.exports = function(core) {
|
|
|
56
55
|
const value = mysqlInstr.getValueFromArgs(args);
|
|
57
56
|
if (!value) return;
|
|
58
57
|
|
|
59
|
-
const sinkContext =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
const sinkContext = {
|
|
59
|
+
name,
|
|
60
|
+
value,
|
|
61
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
62
|
+
};
|
|
63
63
|
|
|
64
64
|
inputTracing.handleSqlInjection(sourceContext, sinkContext);
|
|
65
65
|
}
|
|
@@ -22,7 +22,6 @@ module.exports = function(core) {
|
|
|
22
22
|
const {
|
|
23
23
|
depHooks,
|
|
24
24
|
patcher,
|
|
25
|
-
captureStacktrace,
|
|
26
25
|
protect,
|
|
27
26
|
protect: { inputTracing }
|
|
28
27
|
} = core;
|
|
@@ -40,10 +39,11 @@ module.exports = function(core) {
|
|
|
40
39
|
const value = getQueryFromArgs(args);
|
|
41
40
|
if (!value) return;
|
|
42
41
|
|
|
43
|
-
const sinkContext =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
const sinkContext = {
|
|
43
|
+
name,
|
|
44
|
+
value,
|
|
45
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
46
|
+
};
|
|
47
47
|
|
|
48
48
|
inputTracing.handleSqlInjection(sourceContext, sinkContext);
|
|
49
49
|
}
|
|
@@ -23,7 +23,6 @@ module.exports = function(core) {
|
|
|
23
23
|
scopes: { instrumentation },
|
|
24
24
|
patcher,
|
|
25
25
|
depHooks,
|
|
26
|
-
captureStacktrace,
|
|
27
26
|
protect,
|
|
28
27
|
protect: { inputTracing }
|
|
29
28
|
} = core;
|
|
@@ -49,10 +48,12 @@ module.exports = function(core) {
|
|
|
49
48
|
const value = getQueryFromArgs(args);
|
|
50
49
|
if (!value) return;
|
|
51
50
|
|
|
52
|
-
const sinkContext =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
const sinkContext = {
|
|
52
|
+
name,
|
|
53
|
+
value,
|
|
54
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
55
|
+
};
|
|
56
|
+
|
|
56
57
|
inputTracing.handleSqlInjection(sourceContext, sinkContext);
|
|
57
58
|
}
|
|
58
59
|
});
|
|
@@ -23,7 +23,6 @@ module.exports = function(core) {
|
|
|
23
23
|
scopes: { instrumentation },
|
|
24
24
|
patcher,
|
|
25
25
|
depHooks,
|
|
26
|
-
captureStacktrace,
|
|
27
26
|
protect,
|
|
28
27
|
protect: { inputTracing }
|
|
29
28
|
} = core;
|
|
@@ -44,10 +43,11 @@ module.exports = function(core) {
|
|
|
44
43
|
const value = args[0];
|
|
45
44
|
if (!value || !isString(value)) return;
|
|
46
45
|
|
|
47
|
-
const sinkContext =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
const sinkContext = {
|
|
47
|
+
name,
|
|
48
|
+
value,
|
|
49
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
50
|
+
};
|
|
51
51
|
|
|
52
52
|
inputTracing.handleSqlInjection(sourceContext, sinkContext);
|
|
53
53
|
}
|
|
@@ -23,7 +23,6 @@ module.exports = function(core) {
|
|
|
23
23
|
scopes: { instrumentation },
|
|
24
24
|
patcher,
|
|
25
25
|
depHooks,
|
|
26
|
-
captureStacktrace,
|
|
27
26
|
protect,
|
|
28
27
|
protect: { inputTracing }
|
|
29
28
|
} = core;
|
|
@@ -46,10 +45,11 @@ module.exports = function(core) {
|
|
|
46
45
|
const codeString = args[0];
|
|
47
46
|
if (!codeString || !isString(codeString)) return;
|
|
48
47
|
|
|
49
|
-
const sinkContext =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
const sinkContext = {
|
|
49
|
+
name,
|
|
50
|
+
value: codeString,
|
|
51
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
52
|
+
};
|
|
53
53
|
inputTracing.ssjsInjection(sourceContext, sinkContext);
|
|
54
54
|
}
|
|
55
55
|
});
|
|
@@ -70,14 +70,16 @@ module.exports = function(core) {
|
|
|
70
70
|
|
|
71
71
|
if ((!codeString || !isString(codeString)) && (!isNonEmptyObject(envObj))) return;
|
|
72
72
|
|
|
73
|
-
const codeStringSinkContext = (codeString && isString(codeString)) ?
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
const codeStringSinkContext = (codeString && isString(codeString)) ? {
|
|
74
|
+
name: 'vm.runInNewContext',
|
|
75
|
+
value: codeString,
|
|
76
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] }
|
|
77
|
+
} : null;
|
|
78
|
+
const envObjSinkContext = isNonEmptyObject(envObj) ? {
|
|
79
|
+
name: 'vm.runInNewContext',
|
|
80
|
+
value: envObj,
|
|
81
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] }
|
|
82
|
+
} : null;
|
|
81
83
|
|
|
82
84
|
codeStringSinkContext && inputTracing.ssjsInjection(sourceContext, codeStringSinkContext);
|
|
83
85
|
envObjSinkContext && inputTracing.ssjsInjection(sourceContext, envObjSinkContext);
|
|
@@ -96,10 +98,11 @@ module.exports = function(core) {
|
|
|
96
98
|
const envObj = args[0];
|
|
97
99
|
if (!isNonEmptyObject(envObj)) return;
|
|
98
100
|
|
|
99
|
-
const sinkContext =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const sinkContext = {
|
|
102
|
+
name: 'vm.createContext',
|
|
103
|
+
value: envObj,
|
|
104
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
105
|
+
};
|
|
103
106
|
inputTracing.ssjsInjection(sourceContext, sinkContext);
|
|
104
107
|
}
|
|
105
108
|
});
|
|
@@ -116,10 +119,11 @@ module.exports = function(core) {
|
|
|
116
119
|
const envObj = args[0];
|
|
117
120
|
if (!isNonEmptyObject(envObj)) return;
|
|
118
121
|
|
|
119
|
-
const sinkContext =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
const sinkContext = {
|
|
123
|
+
name: 'vm.Script.prototype.runInNewContext',
|
|
124
|
+
value: envObj,
|
|
125
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
126
|
+
};
|
|
123
127
|
inputTracing.ssjsInjection(sourceContext, sinkContext);
|
|
124
128
|
}
|
|
125
129
|
});
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
module.exports = function(core) {
|
|
19
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
protect: { getPolicy }
|
|
21
|
+
} = core;
|
|
20
22
|
|
|
21
23
|
function makeSourceContext(req, res) {
|
|
22
24
|
// make the abstract request. it is an abstraction of a request that
|
|
@@ -27,8 +29,10 @@ module.exports = function(core) {
|
|
|
27
29
|
// or res objects so that all data coupling is all defined here.
|
|
28
30
|
|
|
29
31
|
// separate path and search params
|
|
30
|
-
|
|
32
|
+
|
|
31
33
|
let uriPath, queries;
|
|
34
|
+
const ix = req.url.indexOf('?');
|
|
35
|
+
|
|
32
36
|
if (ix >= 0) {
|
|
33
37
|
uriPath = req.url.slice(0, ix);
|
|
34
38
|
queries = req.url.slice(ix + 1);
|
|
@@ -36,6 +40,14 @@ module.exports = function(core) {
|
|
|
36
40
|
uriPath = req.url;
|
|
37
41
|
queries = '';
|
|
38
42
|
}
|
|
43
|
+
|
|
44
|
+
const policy = getPolicy({ uriPath });
|
|
45
|
+
|
|
46
|
+
// URL exclusions can disable all rules
|
|
47
|
+
if (!policy) {
|
|
48
|
+
return { allowed: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
// lowercase header keys and capture content-type
|
|
40
52
|
let contentType = '';
|
|
41
53
|
const headers = Array(req.rawHeaders.length);
|
|
@@ -48,15 +60,6 @@ module.exports = function(core) {
|
|
|
48
60
|
}
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
// if it can be determined that qs-type parsing is not being done then set
|
|
52
|
-
// standardUrlParsing to true. if it is true, then the query params and bodies
|
|
53
|
-
// that are form-url-encoded will be parsed by agent-lib and will not need to
|
|
54
|
-
// be parsed separately.
|
|
55
|
-
//
|
|
56
|
-
// the code that scans the dependencies is probably the best place to make the
|
|
57
|
-
// determination.
|
|
58
|
-
const standardUrlParsing = false;
|
|
59
|
-
|
|
60
63
|
// contains request data and information derived from request data. it's
|
|
61
64
|
// possible for any derived information to be derived later, but doing
|
|
62
65
|
// so here is typically better; it makes clear what information is used to
|
|
@@ -69,7 +72,6 @@ module.exports = function(core) {
|
|
|
69
72
|
uriPath,
|
|
70
73
|
queries,
|
|
71
74
|
contentType,
|
|
72
|
-
standardUrlParsing,
|
|
73
75
|
};
|
|
74
76
|
|
|
75
77
|
//
|
|
@@ -80,7 +82,7 @@ module.exports = function(core) {
|
|
|
80
82
|
// block closure captures res so it isn't exposed to beyond here
|
|
81
83
|
block: core.protect.makeResponseBlocker(res),
|
|
82
84
|
|
|
83
|
-
policy
|
|
85
|
+
policy,
|
|
84
86
|
|
|
85
87
|
exclusions: [],
|
|
86
88
|
virtualPatchesEvaluators: [],
|
package/lib/policy.js
CHANGED
|
@@ -27,14 +27,76 @@ const {
|
|
|
27
27
|
Event: { SERVER_SETTINGS_UPDATE },
|
|
28
28
|
} = require('@contrast/common');
|
|
29
29
|
|
|
30
|
-
|
|
31
30
|
module.exports = function(core) {
|
|
32
|
-
const {
|
|
33
|
-
|
|
31
|
+
const {
|
|
32
|
+
config,
|
|
33
|
+
logger,
|
|
34
|
+
messages,
|
|
35
|
+
protect,
|
|
36
|
+
protect: { agentLib }
|
|
37
|
+
} = core;
|
|
38
|
+
|
|
39
|
+
function initCompiled() {
|
|
40
|
+
return {
|
|
41
|
+
url: [],
|
|
42
|
+
querystring: [],
|
|
43
|
+
header: [],
|
|
44
|
+
body: [],
|
|
45
|
+
cookie: [],
|
|
46
|
+
parameter: [],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let compiled = initCompiled();
|
|
51
|
+
|
|
52
|
+
const policy = protect.policy = {
|
|
53
|
+
exclusions: compiled
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function regExpCheck(str) {
|
|
57
|
+
return str.indexOf('*') > 0 ||
|
|
58
|
+
str.indexOf('.') > 0 ||
|
|
59
|
+
str.indexOf('+') > 0 ||
|
|
60
|
+
str.indexOf('?') > 0 ||
|
|
61
|
+
str.indexOf('\\') > 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildUriPathRegExp(urls) {
|
|
65
|
+
let regExpNeeded = false;
|
|
66
|
+
for (const url of urls) {
|
|
67
|
+
if (regExpCheck(url)) {
|
|
68
|
+
regExpNeeded = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (regExpNeeded) {
|
|
72
|
+
const rx = new RegExp(`^${urls.join('|')}$`);
|
|
73
|
+
|
|
74
|
+
return (uriPath) => rx ? rx.test(uriPath) : false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (uriPath) => urls.some((url) => url === uriPath);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createUriPathMatcher(urls) {
|
|
81
|
+
if (urls.length) {
|
|
82
|
+
return buildUriPathRegExp(urls);
|
|
83
|
+
} else {
|
|
84
|
+
return () => true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createInputNameMatcher(dtmInputName) {
|
|
89
|
+
if (regExpCheck(dtmInputName)) {
|
|
90
|
+
const rx = new RegExp(`^${dtmInputName}$`);
|
|
91
|
+
return (inputName) => rx ? rx.test(inputName) : false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (inputName) => inputName === dtmInputName;
|
|
95
|
+
}
|
|
34
96
|
|
|
35
97
|
function getModeFromConfig(ruleId) {
|
|
36
98
|
if (config.protect.disabled_rules.includes(ruleId)) {
|
|
37
|
-
return
|
|
99
|
+
return OFF;
|
|
38
100
|
}
|
|
39
101
|
return config.protect.rules?.[ruleId]?.mode;
|
|
40
102
|
}
|
|
@@ -84,11 +146,20 @@ module.exports = function(core) {
|
|
|
84
146
|
*/
|
|
85
147
|
function updateRulesMask() {
|
|
86
148
|
let rulesMask = 0;
|
|
87
|
-
|
|
149
|
+
|
|
150
|
+
for (const entry of Object.entries(policy)) {
|
|
151
|
+
let [ruleId] = entry;
|
|
152
|
+
const [, mode] = entry;
|
|
153
|
+
|
|
154
|
+
if (ruleId === 'nosql-injection') {
|
|
155
|
+
ruleId = 'nosql-injection-mongo';
|
|
156
|
+
}
|
|
157
|
+
|
|
88
158
|
if (protect.agentLib.RuleType[ruleId] && mode !== OFF) {
|
|
89
159
|
rulesMask = rulesMask | protect.agentLib.RuleType[ruleId];
|
|
90
160
|
}
|
|
91
161
|
}
|
|
162
|
+
|
|
92
163
|
policy.rulesMask = rulesMask;
|
|
93
164
|
}
|
|
94
165
|
|
|
@@ -96,13 +167,83 @@ module.exports = function(core) {
|
|
|
96
167
|
* This gets called by protect.makeSourceContext(). We return copy of policy to avoid
|
|
97
168
|
* inconsistent behavior if policy is updated during request handling.
|
|
98
169
|
*/
|
|
99
|
-
function getPolicy() {
|
|
100
|
-
|
|
101
|
-
|
|
170
|
+
function getPolicy({ uriPath } = {}) {
|
|
171
|
+
const requestPolicy = {
|
|
172
|
+
exclusions: {
|
|
173
|
+
ignoreQuerystring: false,
|
|
174
|
+
querystringPolicy: null,
|
|
175
|
+
ignoreBody: false,
|
|
176
|
+
bodyPolicy: null,
|
|
177
|
+
header: [],
|
|
178
|
+
cookie: [],
|
|
179
|
+
parameter: [],
|
|
180
|
+
},
|
|
181
|
+
rulesMask: policy.rulesMask,
|
|
182
|
+
};
|
|
102
183
|
|
|
103
|
-
|
|
184
|
+
for (const ruleId of Object.values(Rule)) {
|
|
185
|
+
requestPolicy[ruleId] = policy[ruleId];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// handle exclusions
|
|
189
|
+
for (const [inputType, exclusions] of Object.entries(compiled)) {
|
|
190
|
+
for (const e of exclusions) {
|
|
191
|
+
if (!e.matchesUriPath(uriPath)) continue;
|
|
192
|
+
|
|
193
|
+
// url exclusions
|
|
194
|
+
if (inputType === 'url') {
|
|
195
|
+
// if applies to all rules, there is no policy for the request i.e. disable protect
|
|
196
|
+
if (!e.policy) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// merge exclusion's policy into the request's policy
|
|
201
|
+
for (const key of Object.keys(e.policy)) {
|
|
202
|
+
const value = e.policy[key];
|
|
203
|
+
if (key === 'rulesMask') {
|
|
204
|
+
// this is how to disable rules bitwise
|
|
205
|
+
requestPolicy.rulesMask = requestPolicy.rulesMask & ~value;
|
|
206
|
+
} else {
|
|
207
|
+
requestPolicy[key] = value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} else if (inputType === 'querystring') {
|
|
211
|
+
if (!e.policy) {
|
|
212
|
+
requestPolicy.exclusions.ignoreQuerystring = true;
|
|
213
|
+
} else {
|
|
214
|
+
// merge exclusion's policy into the querystring's policy
|
|
215
|
+
requestPolicy.exclusions.querystringPolicy = requestPolicy.exclusions.querystringPolicy || {};
|
|
216
|
+
for (const key of Object.keys(e.policy)) {
|
|
217
|
+
const value = e.policy[key];
|
|
218
|
+
if (key !== 'rulesMask') {
|
|
219
|
+
requestPolicy.exclusions.querystringPolicy[key] = value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} else if (inputType === 'body') {
|
|
224
|
+
if (!e.policy) {
|
|
225
|
+
requestPolicy.exclusions.ignoreBody = true;
|
|
226
|
+
} else {
|
|
227
|
+
// merge exclusion's policy into the querystring's policy
|
|
228
|
+
requestPolicy.exclusions.bodyPolicy = requestPolicy.exclusions.bodyPolicy || {};
|
|
229
|
+
for (const key of Object.keys(e.policy)) {
|
|
230
|
+
const value = e.policy[key];
|
|
231
|
+
if (key !== 'rulesMask') {
|
|
232
|
+
requestPolicy.exclusions.bodyPolicy[key] = value;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
// copy matching input exclusions into request policy
|
|
238
|
+
requestPolicy.exclusions[inputType].push(e);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return requestPolicy;
|
|
244
|
+
}
|
|
104
245
|
|
|
105
|
-
|
|
246
|
+
function updateGlobalPolicy(remoteSettings) {
|
|
106
247
|
let update;
|
|
107
248
|
|
|
108
249
|
const protectionRules = remoteSettings?.settings?.defend?.protectionRules;
|
|
@@ -128,7 +269,80 @@ module.exports = function(core) {
|
|
|
128
269
|
updateRulesMask();
|
|
129
270
|
logger.info({ policy: protect.policy }, `protect policy updated from ${update}`);
|
|
130
271
|
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function updateExclusions(serverUpdate) {
|
|
275
|
+
const exclusions = [
|
|
276
|
+
...(serverUpdate.settings?.exceptions?.inputExceptions || []),
|
|
277
|
+
...(serverUpdate.settings?.exceptions?.urlExceptions || [])
|
|
278
|
+
].filter((exclusion) => exclusion.modes.includes('defend'));
|
|
279
|
+
|
|
280
|
+
if (!exclusions.length) return;
|
|
281
|
+
compiled = initCompiled();
|
|
282
|
+
|
|
283
|
+
for (const exclusionDtm of exclusions) {
|
|
284
|
+
exclusionDtm.inputType = exclusionDtm.inputType || 'URL';
|
|
285
|
+
|
|
286
|
+
const { name, rules, inputName, urls, inputType } = exclusionDtm;
|
|
287
|
+
const key = inputType.toLowerCase();
|
|
288
|
+
|
|
289
|
+
if (!compiled[key]) continue;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const e = { name };
|
|
293
|
+
e.matchesUriPath = createUriPathMatcher(urls);
|
|
294
|
+
|
|
295
|
+
if (inputName) {
|
|
296
|
+
e.matchesInputName = createInputNameMatcher(inputName);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (rules.length) {
|
|
300
|
+
let rulesMask = 0;
|
|
301
|
+
const exclusionPolicy = {};
|
|
302
|
+
|
|
303
|
+
for (let ruleId of rules) {
|
|
304
|
+
// todo: this doesn't seem to make a difference?
|
|
305
|
+
if (ruleId === 'nosql-injection') {
|
|
306
|
+
ruleId = 'nosql-injection-mongo';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (agentLib.RuleType[ruleId]) {
|
|
310
|
+
Object.assign(exclusionPolicy, { [ruleId]: OFF });
|
|
311
|
+
if (inputType === 'URL') {
|
|
312
|
+
rulesMask = rulesMask | agentLib.RuleType[ruleId];
|
|
313
|
+
exclusionPolicy.rulesMask = rulesMask;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
e.policy = exclusionPolicy;
|
|
319
|
+
}
|
|
320
|
+
if (key === 'cookie') {
|
|
321
|
+
e.checkCookieInHeader = (cookieHeader) => {
|
|
322
|
+
for (const cookiePair of cookieHeader.split(';')) {
|
|
323
|
+
const cookieKey = cookiePair.split('=')[0];
|
|
324
|
+
if (e.matchesInputName(cookieKey)) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return false;
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
compiled[key].push(e);
|
|
334
|
+
} catch (err) {
|
|
335
|
+
logger.error({ err, exclusionDtm }, 'failed to process exclusion');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
messages.on(SERVER_SETTINGS_UPDATE, (msg) => {
|
|
341
|
+
updateGlobalPolicy(msg);
|
|
342
|
+
updateExclusions(msg);
|
|
131
343
|
});
|
|
132
344
|
|
|
345
|
+
initPolicy();
|
|
346
|
+
|
|
133
347
|
return protect.getPolicy = getPolicy;
|
|
134
348
|
};
|
|
@@ -38,12 +38,14 @@ const getRuleResults = function(obj, prop) {
|
|
|
38
38
|
// See files in protect/lib/input-tracing/install/.
|
|
39
39
|
|
|
40
40
|
module.exports = function(core) {
|
|
41
|
-
const { protect: { agentLib, semanticAnalysis, throwSecurityException } } = core;
|
|
41
|
+
const { protect: { agentLib, semanticAnalysis, throwSecurityException }, captureStacktrace } = core;
|
|
42
42
|
|
|
43
43
|
function handleResult(sourceContext, sinkContext, ruleId, mode, finding) {
|
|
44
|
+
const { value, stacktraceData } = sinkContext;
|
|
45
|
+
captureStacktrace(sinkContext, stacktraceData);
|
|
44
46
|
const result = {
|
|
45
47
|
blocked: false,
|
|
46
|
-
findings: { command:
|
|
48
|
+
findings: { command: value },
|
|
47
49
|
sinkContext,
|
|
48
50
|
...finding
|
|
49
51
|
};
|
|
@@ -26,7 +26,6 @@ module.exports = function(core) {
|
|
|
26
26
|
logger,
|
|
27
27
|
protect: { semanticAnalysis },
|
|
28
28
|
protect,
|
|
29
|
-
captureStacktrace
|
|
30
29
|
} = core;
|
|
31
30
|
|
|
32
31
|
function install() {
|
|
@@ -60,10 +59,11 @@ module.exports = function(core) {
|
|
|
60
59
|
// see: https://help.semmle.com/wiki/display/JS/XML+external+entity+expansion
|
|
61
60
|
if (!sourceContext || !value || !isString(value) || !args[1].noent) return;
|
|
62
61
|
|
|
63
|
-
const sinkContext =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
const sinkContext = {
|
|
63
|
+
name: 'libxmljs.parseXmlString',
|
|
64
|
+
value,
|
|
65
|
+
stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
|
|
66
|
+
};
|
|
67
67
|
|
|
68
68
|
try {
|
|
69
69
|
semanticAnalysis.handleXXE(sourceContext, sinkContext);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/protect",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
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,9 +18,9 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@contrast/agent-lib": "^5.1.0",
|
|
21
|
-
"@contrast/common": "1.1.
|
|
22
|
-
"@contrast/core": "1.7.
|
|
23
|
-
"@contrast/esm-hooks": "1.3.
|
|
21
|
+
"@contrast/common": "1.1.5",
|
|
22
|
+
"@contrast/core": "1.7.1",
|
|
23
|
+
"@contrast/esm-hooks": "1.3.1",
|
|
24
24
|
"@contrast/scopes": "1.2.0",
|
|
25
25
|
"ipaddr.js": "^2.0.1",
|
|
26
26
|
"semver": "^7.3.7"
|