@contrast/protect 1.67.0 → 1.68.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/hardening/handlers.js +4 -5
- package/lib/input-analysis/handlers.js +210 -204
- package/lib/input-tracing/handlers.js +2 -2
- package/lib/make-source-context.js +5 -7
- package/lib/policy.js +130 -95
- package/lib/semantic-analysis/handlers.js +5 -6
- package/package.json +10 -10
|
@@ -42,11 +42,10 @@ module.exports = function(core) {
|
|
|
42
42
|
return results;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function handleFindings(sourceContext, sinkContext, ruleId, result, findings) {
|
|
45
|
+
function handleFindings(sourceContext, sinkContext, ruleId, result, findings, mode) {
|
|
46
46
|
const { stacktraceOpts } = sinkContext;
|
|
47
|
-
captureStacktrace(sinkContext, stacktraceOpts);
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
captureStacktrace(sinkContext, stacktraceOpts);
|
|
50
49
|
getResults(sourceContext, ruleId).push(result);
|
|
51
50
|
|
|
52
51
|
let blockInfo;
|
|
@@ -63,7 +62,7 @@ module.exports = function(core) {
|
|
|
63
62
|
|
|
64
63
|
hardening.handleUntrustedDeserialization = function (sourceContext, sinkContext) {
|
|
65
64
|
const ruleId = UNTRUSTED_DESERIALIZATION;
|
|
66
|
-
const mode = sourceContext.policy
|
|
65
|
+
const mode = sourceContext.policy.getRuleMode(ruleId);
|
|
67
66
|
const { name, value } = sinkContext;
|
|
68
67
|
|
|
69
68
|
if (
|
|
@@ -82,7 +81,7 @@ module.exports = function(core) {
|
|
|
82
81
|
};
|
|
83
82
|
const findings = { deserializer: name, command: false };
|
|
84
83
|
|
|
85
|
-
handleFindings(sourceContext, sinkContext, ruleId, result, findings);
|
|
84
|
+
handleFindings(sourceContext, sinkContext, ruleId, result, findings, mode);
|
|
86
85
|
};
|
|
87
86
|
|
|
88
87
|
return hardening;
|
|
@@ -32,6 +32,7 @@ const {
|
|
|
32
32
|
}
|
|
33
33
|
} = require('@contrast/common');
|
|
34
34
|
const { Core } = require('@contrast/core/lib/ioc/core');
|
|
35
|
+
|
|
35
36
|
//
|
|
36
37
|
// these rules are not implemented by agent-lib, but are being considered for
|
|
37
38
|
// implementation:
|
|
@@ -133,6 +134,124 @@ module.exports = Core.makeComponent({
|
|
|
133
134
|
// inputs against rules 1) is very fast and 2) dramatically pares down the number
|
|
134
135
|
// of exclusion checks that need to be made.
|
|
135
136
|
|
|
137
|
+
/**
|
|
138
|
+
* merge new findings into the existing findings
|
|
139
|
+
*
|
|
140
|
+
* @param {Object} sourceContext sourceContext.findings is the existing findings
|
|
141
|
+
* @param {Object} newFindings the findings, in {trackRequest, resultsList} format.
|
|
142
|
+
* @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
|
|
143
|
+
*/
|
|
144
|
+
function mergeFindings(sourceContext, newFindings) {
|
|
145
|
+
const { policy } = sourceContext;
|
|
146
|
+
const { securityException, resultsMap } = sourceContext;
|
|
147
|
+
|
|
148
|
+
if (!newFindings.trackRequest) {
|
|
149
|
+
return securityException;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
newFindings.resultsList = newFindings.resultsList.filter(
|
|
153
|
+
(result) => !inputAnalysis.isResultExcluded(sourceContext, result)
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
normalizeFindings(policy, newFindings);
|
|
157
|
+
|
|
158
|
+
sourceContext.trackRequest = sourceContext.trackRequest || newFindings.trackRequest;
|
|
159
|
+
sourceContext.securityException = sourceContext.securityException || newFindings.securityException;
|
|
160
|
+
|
|
161
|
+
// merge them into a ruleId-indexed map (pojo)
|
|
162
|
+
for (const result of newFindings.resultsList) {
|
|
163
|
+
if (!resultsMap[result.ruleId]) {
|
|
164
|
+
resultsMap[result.ruleId] = [];
|
|
165
|
+
}
|
|
166
|
+
resultsMap[result.ruleId].push(result);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return sourceContext.securityException;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
//
|
|
173
|
+
// add common fields to findings.
|
|
174
|
+
//
|
|
175
|
+
function normalizeFindings(policy, findings) {
|
|
176
|
+
// now both augment the rules and check to see if any require blocking
|
|
177
|
+
// at perimeter.
|
|
178
|
+
for (const r of findings.resultsList) {
|
|
179
|
+
// augment
|
|
180
|
+
// what additional augmentations are needed?
|
|
181
|
+
// the name/id might need to be mapped but keep the original so it's not lost
|
|
182
|
+
r.mappedId = agentLibRuleTypeToName[r.ruleId] || r.ruleId;
|
|
183
|
+
|
|
184
|
+
// if we block this or the value is found in sink, we'll know not to check
|
|
185
|
+
// this result for probe analysis in handleRequestEnd().
|
|
186
|
+
r.blocked = false;
|
|
187
|
+
r.exploited = false;
|
|
188
|
+
|
|
189
|
+
// apply exclusions here.
|
|
190
|
+
//
|
|
191
|
+
// apply exclusions after scoring inputs as it will require less work
|
|
192
|
+
// most of the time.
|
|
193
|
+
//
|
|
194
|
+
// the following might need to be changed. BAP is legacy behavior; beyond that,
|
|
195
|
+
// the only way a score >= 90 can come back is if there is no "worth-watching"
|
|
196
|
+
// option and that implies that there is no sink, so this is the only place at
|
|
197
|
+
// which the block can occur. so at a minimum 'block' should also result in a
|
|
198
|
+
// block.
|
|
199
|
+
const mode = policy.getRuleMode(r.ruleId);
|
|
200
|
+
|
|
201
|
+
if (r.score >= 90 && BLOCKING_MODES.includes(mode)) {
|
|
202
|
+
r.blocked = true;
|
|
203
|
+
findings.securityException = [mode, r.ruleId, { result: r }];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function checkIpsMatch(listEntry, ip) {
|
|
209
|
+
const parsed = address.process(ip);
|
|
210
|
+
|
|
211
|
+
// Check if IP is in CIDR range,
|
|
212
|
+
if (listEntry.cidr) {
|
|
213
|
+
if (parsed.kind() !== listEntry.cidr.kind) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (parsed.match(listEntry.cidr.range)) {
|
|
218
|
+
return { ...listEntry, match: ip };
|
|
219
|
+
} else {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// or do a direct comparison
|
|
225
|
+
if (parsed.toNormalizedString() === listEntry.normalizedValue) {
|
|
226
|
+
return { ...listEntry, matchedIp: ip };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* getValueAtKey() is used to fetch the object (expected) associated
|
|
234
|
+
* with the path of keys in obj. i say expected because this is only used
|
|
235
|
+
* for fetching the objects associated with a nosql vulnerability and those
|
|
236
|
+
* should always be objects.
|
|
237
|
+
*
|
|
238
|
+
* @param {Object} obj an object with keys
|
|
239
|
+
* @param {Array} path list of keys to walk through the object
|
|
240
|
+
* @param {String} lastKey the last key (it's not in path)
|
|
241
|
+
*
|
|
242
|
+
* @returns the value at end of walking path in obj
|
|
243
|
+
*/
|
|
244
|
+
function getValueAtKey(obj, path, key) {
|
|
245
|
+
for (const p of path) {
|
|
246
|
+
/* c8 ignore next 6 */
|
|
247
|
+
if (!(p in obj)) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
obj = obj[p];
|
|
251
|
+
}
|
|
252
|
+
return key in obj ? obj[key] : undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
136
255
|
/**
|
|
137
256
|
* handleConnect()
|
|
138
257
|
*
|
|
@@ -170,7 +289,7 @@ module.exports = Core.makeComponent({
|
|
|
170
289
|
* @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
|
|
171
290
|
*/
|
|
172
291
|
inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
|
|
173
|
-
const
|
|
292
|
+
const rulesMask = sourceContext.policy.getRulesMask();
|
|
174
293
|
|
|
175
294
|
inputAnalysis.handleVirtualPatches(
|
|
176
295
|
sourceContext,
|
|
@@ -210,16 +329,13 @@ module.exports = Core.makeComponent({
|
|
|
210
329
|
inputAnalysis.handleQueryParams = function handleQueryParams(sourceContext, queryParams) {
|
|
211
330
|
if (sourceContext.analyzedQuery) return;
|
|
212
331
|
sourceContext.analyzedQuery = true;
|
|
213
|
-
|
|
214
332
|
if (typeof queryParams !== 'object') {
|
|
215
333
|
logger.debug({ queryParams }, 'handleQueryParams() called with non-object');
|
|
216
334
|
return;
|
|
217
335
|
}
|
|
218
|
-
|
|
219
336
|
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: queryParams });
|
|
220
337
|
|
|
221
338
|
const block = commonObjectAnalyzer(sourceContext, queryParams, parameterInputTypes);
|
|
222
|
-
|
|
223
339
|
if (block) {
|
|
224
340
|
core.protect.reportFinding(block[2]);
|
|
225
341
|
core.protect.throwSecurityException(sourceContext);
|
|
@@ -236,6 +352,9 @@ module.exports = Core.makeComponent({
|
|
|
236
352
|
* @param {Object} urlParams pojo
|
|
237
353
|
*/
|
|
238
354
|
inputAnalysis.handleUrlParams = function(sourceContext, urlParams) {
|
|
355
|
+
const rulesMask = sourceContext.policy.getRulesMask();
|
|
356
|
+
if (!rulesMask) return;
|
|
357
|
+
|
|
239
358
|
if (sourceContext.analyzedUrlParams) return;
|
|
240
359
|
sourceContext.analyzedUrlParams = true;
|
|
241
360
|
|
|
@@ -246,7 +365,6 @@ module.exports = Core.makeComponent({
|
|
|
246
365
|
|
|
247
366
|
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: urlParams });
|
|
248
367
|
|
|
249
|
-
const { policy: { rulesMask } } = sourceContext;
|
|
250
368
|
const resultsList = [];
|
|
251
369
|
const { UrlParameter } = agentLib.InputType;
|
|
252
370
|
|
|
@@ -257,7 +375,6 @@ module.exports = Core.makeComponent({
|
|
|
257
375
|
}
|
|
258
376
|
|
|
259
377
|
const items = agentLib.scoreAtom(rulesMask, value, UrlParameter, preferWW);
|
|
260
|
-
|
|
261
378
|
if (!items) {
|
|
262
379
|
return;
|
|
263
380
|
}
|
|
@@ -311,7 +428,8 @@ module.exports = Core.makeComponent({
|
|
|
311
428
|
|
|
312
429
|
inputAnalysis.handleVirtualPatches(sourceContext, { HEADERS: cookies });
|
|
313
430
|
|
|
314
|
-
const
|
|
431
|
+
const rulesMask = sourceContext.policy.getRulesMask();
|
|
432
|
+
if (!rulesMask) return;
|
|
315
433
|
|
|
316
434
|
const cookiesArr = Object.entries(cookies).reduce((acc, [key, value]) => {
|
|
317
435
|
// things like booleans will cause agent-lib to throw
|
|
@@ -321,7 +439,6 @@ module.exports = Core.makeComponent({
|
|
|
321
439
|
|
|
322
440
|
const cookieFindings = agentLib.scoreRequestConnect(rulesMask, { cookies: cookiesArr }, preferWW);
|
|
323
441
|
|
|
324
|
-
|
|
325
442
|
const block = mergeFindings(sourceContext, cookieFindings);
|
|
326
443
|
|
|
327
444
|
if (block) {
|
|
@@ -379,7 +496,7 @@ module.exports = Core.makeComponent({
|
|
|
379
496
|
const { policy } = sourceContext;
|
|
380
497
|
const resultsList = [];
|
|
381
498
|
|
|
382
|
-
if (policy
|
|
499
|
+
if (policy.getRuleMode(Rule.UNSAFE_FILE_UPLOAD) === 'off') return;
|
|
383
500
|
|
|
384
501
|
for (const name of names) {
|
|
385
502
|
if (!isString(name)) {
|
|
@@ -387,7 +504,7 @@ module.exports = Core.makeComponent({
|
|
|
387
504
|
return;
|
|
388
505
|
}
|
|
389
506
|
|
|
390
|
-
const items = agentLib.scoreAtom(policy.
|
|
507
|
+
const items = agentLib.scoreAtom(policy.getRulesMask(), name, type);
|
|
391
508
|
|
|
392
509
|
if (!items) {
|
|
393
510
|
return;
|
|
@@ -424,6 +541,7 @@ module.exports = Core.makeComponent({
|
|
|
424
541
|
|
|
425
542
|
if (!Object.keys(requestInput).filter(Boolean).length || !sourceContext?.virtualPatchesEvaluators.length) return;
|
|
426
543
|
|
|
544
|
+
// todo: get virtualPatchesEvaluators from protect policy instead of request
|
|
427
545
|
for (const vpEvaluators of sourceContext.virtualPatchesEvaluators) {
|
|
428
546
|
for (const key in requestInput) {
|
|
429
547
|
const evaluator = vpEvaluators.get(key);
|
|
@@ -502,7 +620,7 @@ module.exports = Core.makeComponent({
|
|
|
502
620
|
|
|
503
621
|
inputAnalysis.handleMethodTampering = function(sourceContext, connectInputs) {
|
|
504
622
|
const ruleId = Rule.METHOD_TAMPERING;
|
|
505
|
-
const mode = sourceContext.policy
|
|
623
|
+
const mode = sourceContext.policy.getRuleMode(ruleId);
|
|
506
624
|
if (mode !== OFF) {
|
|
507
625
|
const { method } = connectInputs;
|
|
508
626
|
|
|
@@ -533,9 +651,10 @@ module.exports = Core.makeComponent({
|
|
|
533
651
|
* @param {Object} sourceContext
|
|
534
652
|
*/
|
|
535
653
|
inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) {
|
|
654
|
+
const { policy } = sourceContext;
|
|
536
655
|
// check status code to verify method-tampering exploitation
|
|
537
656
|
const mtResult = sourceContext.resultsMap[Rule.METHOD_TAMPERING]?.[0];
|
|
538
|
-
if (mtResult) {
|
|
657
|
+
if (mtResult && policy.getRuleMode(Rule.METHOD_TAMPERING) !== OFF) {
|
|
539
658
|
const { statusCode } = sourceContext.resData;
|
|
540
659
|
if (statusCode !== 405 || statusCode !== 501) {
|
|
541
660
|
mtResult.exploited = true;
|
|
@@ -543,12 +662,11 @@ module.exports = Core.makeComponent({
|
|
|
543
662
|
}
|
|
544
663
|
}
|
|
545
664
|
|
|
546
|
-
if (!config.protect.probe_analysis.enable) return;
|
|
547
|
-
|
|
548
|
-
const probeReports = [];
|
|
549
|
-
|
|
550
665
|
// Detecting probes
|
|
551
|
-
const
|
|
666
|
+
const rulesMask = sourceContext.policy.getRulesMask();
|
|
667
|
+
if (rulesMask == 0 || !config.protect.probe_analysis.enable) return;
|
|
668
|
+
const probeReports = [];
|
|
669
|
+
const { resultsMap } = sourceContext;
|
|
552
670
|
const probesRules = [Rule.CMD_INJECTION, Rule.PATH_TRAVERSAL, Rule.SQL_INJECTION, Rule.XXE];
|
|
553
671
|
const probes = {};
|
|
554
672
|
const findingsForScoreRequest = {
|
|
@@ -571,7 +689,7 @@ module.exports = Core.makeComponent({
|
|
|
571
689
|
} = resultByRuleId;
|
|
572
690
|
|
|
573
691
|
if (
|
|
574
|
-
|
|
692
|
+
sourceContext.policy.getRuleMode(ruleId) !== MONITOR ||
|
|
575
693
|
exploited === true || // todo: remove
|
|
576
694
|
score >= 90 ||
|
|
577
695
|
!probesRules.some((rule) => rule === ruleId) ||
|
|
@@ -623,7 +741,7 @@ module.exports = Core.makeComponent({
|
|
|
623
741
|
});
|
|
624
742
|
|
|
625
743
|
results
|
|
626
|
-
.filter(({ score, ruleId }) => score >= 90 &&
|
|
744
|
+
.filter(({ score, ruleId }) => score >= 90 && sourceContext.policy.getRuleMode(ruleId) == MONITOR)
|
|
627
745
|
.forEach((result) => {
|
|
628
746
|
const resultByRuleId = valueToResultByRuleId[result.value];
|
|
629
747
|
const probe = Object.assign({}, resultByRuleId, result, {
|
|
@@ -652,11 +770,80 @@ module.exports = Core.makeComponent({
|
|
|
652
770
|
}
|
|
653
771
|
};
|
|
654
772
|
|
|
773
|
+
/**
|
|
774
|
+
* Reads the source context's policy and compares to result item to check whether to ignore it.
|
|
775
|
+
* @param {ProtectMessage} sourceContext
|
|
776
|
+
* @param {Result} result
|
|
777
|
+
* @returns {boolean} whether result should be excluded
|
|
778
|
+
*/
|
|
779
|
+
inputAnalysis.isResultExcluded = function isResultExcluded(sourceContext, result) {
|
|
780
|
+
const exclusions = sourceContext.policy.getExclusionInfo();
|
|
781
|
+
if (!exclusions) return false;
|
|
782
|
+
|
|
783
|
+
const { ruleId, path, inputType, value } = result;
|
|
784
|
+
const inputName = path ? path[path.length - 1] : null;
|
|
785
|
+
|
|
786
|
+
let checkCookiesInHeader = false;
|
|
787
|
+
let inputExclusions;
|
|
788
|
+
|
|
789
|
+
switch (inputType) {
|
|
790
|
+
case 'JsonKey':
|
|
791
|
+
case 'JsonValue':
|
|
792
|
+
case 'MultipartName': {
|
|
793
|
+
if (
|
|
794
|
+
exclusions?.ignoreBody ||
|
|
795
|
+
exclusions?.bodyPolicy?.[ruleId] == OFF
|
|
796
|
+
) return true;
|
|
797
|
+
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
case 'ParameterKey':
|
|
801
|
+
case 'ParameterValue': {
|
|
802
|
+
const qsExcluded = exclusions.ignoreQuerystring || exclusions.querystringPolicy?.[ruleId] === OFF;
|
|
803
|
+
if (qsExcluded) return true;
|
|
804
|
+
inputExclusions = exclusions.parameter;
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
case 'CookieValue': {
|
|
808
|
+
inputExclusions = exclusions.cookie;
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
case 'HeaderKey':
|
|
812
|
+
case 'HeaderValue': {
|
|
813
|
+
if (path[0] && StringPrototypeToLowerCase.call(path[0]) === 'cookie') {
|
|
814
|
+
inputExclusions = exclusions.cookie;
|
|
815
|
+
checkCookiesInHeader = true;
|
|
816
|
+
} else {
|
|
817
|
+
inputExclusions = exclusions?.header;
|
|
818
|
+
}
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (!inputName || !inputExclusions) return false;
|
|
824
|
+
|
|
825
|
+
for (const excl of inputExclusions) {
|
|
826
|
+
let nameCheck = false;
|
|
827
|
+
if (checkCookiesInHeader) {
|
|
828
|
+
nameCheck = excl.checkCookiesInHeader(value);
|
|
829
|
+
} else {
|
|
830
|
+
nameCheck = excl.matchesInputName(inputName);
|
|
831
|
+
}
|
|
832
|
+
if (!nameCheck) continue;
|
|
833
|
+
if (!excl.policy || excl.policy[ruleId] === OFF) {
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return false;
|
|
839
|
+
};
|
|
840
|
+
|
|
655
841
|
/**
|
|
656
842
|
* commonObjectAnalyzer() walks an object supplied by the end-user and checks
|
|
657
843
|
* it for vulnerabilities.
|
|
658
844
|
*
|
|
659
|
-
*
|
|
845
|
+
*
|
|
846
|
+
This can cause the request to be blocked, depending on the mode and findings.
|
|
660
847
|
*
|
|
661
848
|
* @param {Object} sourceContext the sourceContext for the request
|
|
662
849
|
* @param {Object} object the object to analyze. It could be from any input
|
|
@@ -668,14 +855,14 @@ module.exports = Core.makeComponent({
|
|
|
668
855
|
* @returns {Array | undefined} returns an array with block info if vulnerability was found.
|
|
669
856
|
*/
|
|
670
857
|
function commonObjectAnalyzer(sourceContext, object, inputTypes) {
|
|
671
|
-
const { policy: { rulesMask } } = sourceContext;
|
|
672
|
-
if (!rulesMask) return;
|
|
673
|
-
|
|
674
858
|
// use inputTypes to set params...
|
|
675
859
|
const { keyType, inputType } = inputTypes;
|
|
676
860
|
const inputTypeStr = inputTypes === jsonInputTypes ? 'Json' : 'Parameter';
|
|
677
861
|
const resultsList = [];
|
|
678
862
|
|
|
863
|
+
const rulesMask = sourceContext.policy.getRulesMask();
|
|
864
|
+
if (!rulesMask) return;
|
|
865
|
+
|
|
679
866
|
// it's possible to optimize this if qs (or a similar package) is not loaded
|
|
680
867
|
// or if none of the values of queryParams are objects. a quick '.includes()'
|
|
681
868
|
// could be used to determine that. if none are objects then traverseKeysAndValues()
|
|
@@ -804,184 +991,3 @@ module.exports = Core.makeComponent({
|
|
|
804
991
|
}
|
|
805
992
|
},
|
|
806
993
|
});
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
* Reads the source context's policy and compares to result item to check whether to ignore it.
|
|
810
|
-
* @param {ProtectMessage} sourceContext
|
|
811
|
-
* @param {Result} result
|
|
812
|
-
* @returns {boolean} whether result should be excluded
|
|
813
|
-
*/
|
|
814
|
-
function isResultExcluded(sourceContext, result) {
|
|
815
|
-
const { policy: { exclusions } } = sourceContext;
|
|
816
|
-
const { ruleId, path, inputType, value } = result;
|
|
817
|
-
const inputName = path ? path[path.length - 1] : null;
|
|
818
|
-
|
|
819
|
-
let checkCookiesInHeader = false;
|
|
820
|
-
let inputExclusions;
|
|
821
|
-
switch (inputType) {
|
|
822
|
-
case 'JsonKey':
|
|
823
|
-
case 'JsonValue':
|
|
824
|
-
case 'MultipartName': {
|
|
825
|
-
return exclusions.ignoreBody || exclusions.bodyPolicy?.[ruleId] === OFF;
|
|
826
|
-
}
|
|
827
|
-
case 'ParameterKey':
|
|
828
|
-
case 'ParameterValue': {
|
|
829
|
-
const qsExcluded = exclusions.ignoreQuerystring || exclusions.querystringPolicy?.[ruleId] === OFF;
|
|
830
|
-
if (qsExcluded) return true;
|
|
831
|
-
inputExclusions = exclusions.parameter;
|
|
832
|
-
break;
|
|
833
|
-
}
|
|
834
|
-
case 'CookieValue': {
|
|
835
|
-
inputExclusions = exclusions.cookie;
|
|
836
|
-
break;
|
|
837
|
-
}
|
|
838
|
-
case 'HeaderKey':
|
|
839
|
-
case 'HeaderValue': {
|
|
840
|
-
if (path[0] && StringPrototypeToLowerCase.call(path[0]) === 'cookie') {
|
|
841
|
-
inputExclusions = exclusions.cookie;
|
|
842
|
-
checkCookiesInHeader = true;
|
|
843
|
-
} else {
|
|
844
|
-
inputExclusions = exclusions.header;
|
|
845
|
-
}
|
|
846
|
-
break;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
if (!inputName || !inputExclusions) return false;
|
|
851
|
-
|
|
852
|
-
for (const excl of inputExclusions) {
|
|
853
|
-
let nameCheck = false;
|
|
854
|
-
if (checkCookiesInHeader) {
|
|
855
|
-
nameCheck = excl.checkCookiesInHeader(value);
|
|
856
|
-
} else {
|
|
857
|
-
nameCheck = excl.matchesInputName(inputName);
|
|
858
|
-
}
|
|
859
|
-
if (!nameCheck) continue;
|
|
860
|
-
if (!excl.policy || excl.policy[ruleId] === OFF) {
|
|
861
|
-
return true;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
return false;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
/**
|
|
869
|
-
* merge new findings into the existing findings
|
|
870
|
-
*
|
|
871
|
-
* @param {Object} sourceContext sourceContext.findings is the existing findings
|
|
872
|
-
* @param {Object} newFindings the findings, in {trackRequest, resultsList} format.
|
|
873
|
-
* @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
|
|
874
|
-
*/
|
|
875
|
-
function mergeFindings(sourceContext, newFindings) {
|
|
876
|
-
const { policy, securityException, resultsMap } = sourceContext;
|
|
877
|
-
|
|
878
|
-
if (!newFindings.trackRequest) {
|
|
879
|
-
return securityException;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
newFindings.resultsList = newFindings.resultsList.filter(
|
|
883
|
-
(result) => !isResultExcluded(sourceContext, result)
|
|
884
|
-
);
|
|
885
|
-
|
|
886
|
-
normalizeFindings(policy, newFindings);
|
|
887
|
-
|
|
888
|
-
sourceContext.trackRequest = sourceContext.trackRequest || newFindings.trackRequest;
|
|
889
|
-
sourceContext.securityException = sourceContext.securityException || newFindings.securityException;
|
|
890
|
-
|
|
891
|
-
// merge them into a ruleId-indexed map (pojo)
|
|
892
|
-
for (const result of newFindings.resultsList) {
|
|
893
|
-
if (!resultsMap[result.ruleId]) {
|
|
894
|
-
resultsMap[result.ruleId] = [];
|
|
895
|
-
}
|
|
896
|
-
resultsMap[result.ruleId].push(result);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
return sourceContext.securityException;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
//
|
|
903
|
-
// add common fields to findings.
|
|
904
|
-
//
|
|
905
|
-
function normalizeFindings(policy, findings) {
|
|
906
|
-
// now both augment the rules and check to see if any require blocking
|
|
907
|
-
// at perimeter.
|
|
908
|
-
for (const r of findings.resultsList) {
|
|
909
|
-
// augment
|
|
910
|
-
// what additional augmentations are needed?
|
|
911
|
-
// the name/id might need to be mapped but keep the original so it's not lost
|
|
912
|
-
r.mappedId = agentLibRuleTypeToName[r.ruleId] || r.ruleId;
|
|
913
|
-
|
|
914
|
-
// if we block this or the value is found in sink, we'll know not to check
|
|
915
|
-
// this result for probe analysis in handleRequestEnd().
|
|
916
|
-
r.blocked = false;
|
|
917
|
-
r.exploited = false;
|
|
918
|
-
|
|
919
|
-
// apply exclusions here.
|
|
920
|
-
//
|
|
921
|
-
// apply exclusions after scoring inputs as it will require less work
|
|
922
|
-
// most of the time.
|
|
923
|
-
//
|
|
924
|
-
// the following might need to be changed. BAP is legacy behavior; beyond that,
|
|
925
|
-
// the only way a score >= 90 can come back is if there is no "worth-watching"
|
|
926
|
-
// option and that implies that there is no sink, so this is the only place at
|
|
927
|
-
// which the block can occur. so at a minimum 'block' should also result in a
|
|
928
|
-
// block.
|
|
929
|
-
const mode = policy[r.ruleId];
|
|
930
|
-
if (r.score >= 90 && BLOCKING_MODES.includes(mode)) {
|
|
931
|
-
r.blocked = true;
|
|
932
|
-
findings.securityException = [mode, r.ruleId, { result: r }];
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
function checkIpsMatch(listEntry, ip) {
|
|
939
|
-
const parsed = address.process(ip);
|
|
940
|
-
|
|
941
|
-
// Check if IP is in CIDR range,
|
|
942
|
-
if (listEntry.cidr) {
|
|
943
|
-
if (parsed.kind() !== listEntry.cidr.kind) {
|
|
944
|
-
return null;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
if (parsed.match(listEntry.cidr.range)) {
|
|
948
|
-
return { ...listEntry, match: ip };
|
|
949
|
-
} else {
|
|
950
|
-
return null;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// or do a direct comparison
|
|
955
|
-
if (parsed.toNormalizedString() === listEntry.normalizedValue) {
|
|
956
|
-
return { ...listEntry, matchedIp: ip };
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
return null;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
/**
|
|
963
|
-
* getValueAtKey() is used to fetch the object (expected) associated
|
|
964
|
-
* with the path of keys in obj. i say expected because this is only used
|
|
965
|
-
* for fetching the objects associated with a nosql vulnerability and those
|
|
966
|
-
* should always be objects.
|
|
967
|
-
*
|
|
968
|
-
* @param {Object} obj an object with keys
|
|
969
|
-
* @param {Array} path list of keys to walk through the object
|
|
970
|
-
* @param {String} lastKey the last key (it's not in path)
|
|
971
|
-
*
|
|
972
|
-
* @returns the value at end of walking path in obj
|
|
973
|
-
*/
|
|
974
|
-
function getValueAtKey(obj, path, key) {
|
|
975
|
-
for (const p of path) {
|
|
976
|
-
/* c8 ignore next 6 */
|
|
977
|
-
if (!(p in obj)) {
|
|
978
|
-
return undefined;
|
|
979
|
-
}
|
|
980
|
-
obj = obj[p];
|
|
981
|
-
}
|
|
982
|
-
return key in obj ? obj[key] : undefined;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function isMonitorMode(ruleId, sourceContext) {
|
|
986
|
-
return sourceContext.policy[ruleId] === MONITOR;
|
|
987
|
-
}
|
|
@@ -43,7 +43,7 @@ module.exports = function(core) {
|
|
|
43
43
|
captureStacktrace(sinkContext, stacktraceOpts);
|
|
44
44
|
result.exploited = true;
|
|
45
45
|
|
|
46
|
-
const mode = sourceContext.policy
|
|
46
|
+
const mode = sourceContext.policy.getRuleMode(ruleId);
|
|
47
47
|
const eventArg = { findings, result, sinkContext };
|
|
48
48
|
|
|
49
49
|
let blockInfo;
|
|
@@ -328,7 +328,7 @@ module.exports = function(core) {
|
|
|
328
328
|
* @returns {AnalysisResult[]}
|
|
329
329
|
*/
|
|
330
330
|
function getResultsByRuleId(ruleId, context) {
|
|
331
|
-
if (!context.policy || context.policy
|
|
331
|
+
if (!context.policy || context.policy.getRuleMode(ruleId) === OFF) {
|
|
332
332
|
return;
|
|
333
333
|
}
|
|
334
334
|
// because agent-lib stores all nosql-injection results under nosql-injection-mongo
|
|
@@ -18,8 +18,6 @@
|
|
|
18
18
|
module.exports = function(core) {
|
|
19
19
|
const { protect } = core;
|
|
20
20
|
|
|
21
|
-
const DISABLED_POLICY = { allowed: true };
|
|
22
|
-
|
|
23
21
|
/**
|
|
24
22
|
* @param {object} param
|
|
25
23
|
* @param {object} param.store
|
|
@@ -33,12 +31,7 @@ module.exports = function(core) {
|
|
|
33
31
|
// incomingMessage,
|
|
34
32
|
serverResponse,
|
|
35
33
|
}) {
|
|
36
|
-
if (!core.config.getEffectiveValue('protect.enable')) return DISABLED_POLICY;
|
|
37
|
-
|
|
38
34
|
const policy = protect.getPolicy({ uriPath: sourceInfo.uriPath });
|
|
39
|
-
// URL exclusions can disable all rules
|
|
40
|
-
if (!policy || policy.rulesMask === 0) return DISABLED_POLICY;
|
|
41
|
-
|
|
42
35
|
const protectStore = {
|
|
43
36
|
resData: {
|
|
44
37
|
statusCode: null,
|
|
@@ -56,6 +49,11 @@ module.exports = function(core) {
|
|
|
56
49
|
resultsMap: Object.create(null),
|
|
57
50
|
};
|
|
58
51
|
|
|
52
|
+
if (policy.allowed) {
|
|
53
|
+
protectStore.allowed = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
59
57
|
return protectStore;
|
|
60
58
|
}
|
|
61
59
|
|
package/lib/policy.js
CHANGED
|
@@ -24,10 +24,10 @@ const {
|
|
|
24
24
|
StringPrototypeToLowerCase,
|
|
25
25
|
StringPrototypeSplit,
|
|
26
26
|
RegExpPrototypeTest
|
|
27
|
-
}
|
|
27
|
+
},
|
|
28
|
+
set,
|
|
28
29
|
} = require('@contrast/common');
|
|
29
30
|
const { ConfigSource } = require('@contrast/config');
|
|
30
|
-
|
|
31
31
|
const { BLOCK_AT_PERIMETER, OFF } = ProtectRuleMode;
|
|
32
32
|
const {
|
|
33
33
|
BOT_BLOCKER,
|
|
@@ -58,6 +58,121 @@ module.exports = function (core) {
|
|
|
58
58
|
protect: { agentLib }
|
|
59
59
|
} = core;
|
|
60
60
|
|
|
61
|
+
// todo: can we not init this and just set what's needed
|
|
62
|
+
let processedExclusions = initCompiled();
|
|
63
|
+
|
|
64
|
+
const policy = protect.policy = {
|
|
65
|
+
version: Date.now(),
|
|
66
|
+
exclusions: processedExclusions
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class RequestPolicy {
|
|
71
|
+
constructor(core, sourceInfo) {
|
|
72
|
+
Object.defineProperty(this, 'core', { value: core });
|
|
73
|
+
Object.defineProperty(this, 'sourceInfo', { value: sourceInfo });
|
|
74
|
+
|
|
75
|
+
this.init();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
init() {
|
|
79
|
+
const { uriPath } = this.sourceInfo;
|
|
80
|
+
this.version = core.protect.policy.version;
|
|
81
|
+
|
|
82
|
+
if (!this.core.config.getEffectiveValue('protect.enable')) {
|
|
83
|
+
this.allowed = true;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// todo build exclusions
|
|
88
|
+
for (const [inputType, exclusions] of Object.entries(processedExclusions)) {
|
|
89
|
+
for (const e of exclusions) {
|
|
90
|
+
if (!e.matchesUriPath(uriPath)) continue;
|
|
91
|
+
|
|
92
|
+
// url exclusions
|
|
93
|
+
if (inputType === 'url') {
|
|
94
|
+
// if applies to all rules, there is no policy for the request i.e. disable protect
|
|
95
|
+
if (!e.policy) {
|
|
96
|
+
this.allowed = true;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// merge exclusion's policy into the request's policy
|
|
101
|
+
for (const key of Object.keys(e.policy)) {
|
|
102
|
+
const value = e.policy[key];
|
|
103
|
+
if (key === 'rulesMask') {
|
|
104
|
+
if (this.exclusions?.rulesMask == null)
|
|
105
|
+
set(this, 'exclusions.rulesMask', this.core.protect.policy.rulesMask);
|
|
106
|
+
// this is how to disable rules bitwise
|
|
107
|
+
this.exclusions.rulesMask = this.exclusions.rulesMask & ~value;
|
|
108
|
+
} else {
|
|
109
|
+
set(this, `exclusions.${key}`, value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} else if (inputType === 'querystring') {
|
|
113
|
+
if (!e.policy) {
|
|
114
|
+
set(this, 'exclusions.ignoreQuerystring', true);
|
|
115
|
+
} else {
|
|
116
|
+
// merge exclusion's policy into the querystring's policy
|
|
117
|
+
// this.exclusions.querystringPolicy = this.exclusions.querystringPolicy || {};
|
|
118
|
+
for (const key of Object.keys(e.policy)) {
|
|
119
|
+
const value = e.policy[key];
|
|
120
|
+
if (key !== 'rulesMask') {
|
|
121
|
+
set(this, `exclusions.querystringPolicy.${key}`, value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} else if (inputType === 'body') {
|
|
126
|
+
if (!e.policy) {
|
|
127
|
+
set(this, 'exclusions.ignoreBody', true);
|
|
128
|
+
} else {
|
|
129
|
+
// merge exclusion's policy into the querystring's policy
|
|
130
|
+
// set(this, `exclusions.bodyPolicy = this.exclusions.bodyPolicy || {};
|
|
131
|
+
for (const key of Object.keys(e.policy)) {
|
|
132
|
+
const value = e.policy[key];
|
|
133
|
+
if (key !== 'rulesMask') {
|
|
134
|
+
set(this, `exclusions.bodyPolicy.${key}`, value);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
// copy matching input exclusions into request policy
|
|
140
|
+
if (!this.exclusions?.[inputType]) set(this, `exclusions.${inputType}`, []);
|
|
141
|
+
this.exclusions[inputType].push(e);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
checkInit() {
|
|
148
|
+
if (!this.version == core.protect.policy.version) {
|
|
149
|
+
this.init();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
isDisabled() {
|
|
154
|
+
this.checkInit();
|
|
155
|
+
return this.allowed === true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getRulesMask(inputType) {
|
|
159
|
+
this.checkInit();
|
|
160
|
+
if (this.allowed) return 0;
|
|
161
|
+
return this.exclusions?.rulesMask ?? this.core.protect.policy.rulesMask;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
getRuleMode(ruleId) {
|
|
165
|
+
this.checkInit();
|
|
166
|
+
if (this.allowed) return OFF;
|
|
167
|
+
return this.exclusions?.[ruleId] ?? this.core.protect.policy[ruleId];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
getExclusionInfo(key, inputType) {
|
|
171
|
+
this.checkInit();
|
|
172
|
+
return key ? this.exclusions?.[key] : this.exclusions;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
61
176
|
function initCompiled() {
|
|
62
177
|
return {
|
|
63
178
|
url: [],
|
|
@@ -69,12 +184,6 @@ module.exports = function (core) {
|
|
|
69
184
|
};
|
|
70
185
|
}
|
|
71
186
|
|
|
72
|
-
let compiled = initCompiled();
|
|
73
|
-
|
|
74
|
-
const policy = protect.policy = {
|
|
75
|
-
exclusions: compiled
|
|
76
|
-
};
|
|
77
|
-
|
|
78
187
|
function regExpCheck(str) {
|
|
79
188
|
return str.indexOf('*') > 0 ||
|
|
80
189
|
str.indexOf('.') > 0 ||
|
|
@@ -156,96 +265,19 @@ module.exports = function (core) {
|
|
|
156
265
|
ruleId = 'nosql-injection-mongo';
|
|
157
266
|
}
|
|
158
267
|
|
|
159
|
-
if (
|
|
160
|
-
rulesMask = rulesMask |
|
|
268
|
+
if (agentLib.RuleType[ruleId] && mode !== OFF) {
|
|
269
|
+
rulesMask = rulesMask | agentLib.RuleType[ruleId];
|
|
161
270
|
}
|
|
162
271
|
}
|
|
163
272
|
|
|
164
273
|
policy.rulesMask = rulesMask;
|
|
165
274
|
}
|
|
166
275
|
|
|
167
|
-
/**
|
|
168
|
-
* This gets called by protect.makeSourceContext(). We return copy of policy to avoid
|
|
169
|
-
* inconsistent behavior if policy is updated during request handling.
|
|
170
|
-
*/
|
|
171
|
-
function getPolicy({ uriPath } = {}) {
|
|
172
|
-
const requestPolicy = {
|
|
173
|
-
exclusions: {
|
|
174
|
-
ignoreQuerystring: false,
|
|
175
|
-
querystringPolicy: null,
|
|
176
|
-
ignoreBody: false,
|
|
177
|
-
bodyPolicy: null,
|
|
178
|
-
header: [],
|
|
179
|
-
cookie: [],
|
|
180
|
-
parameter: [],
|
|
181
|
-
},
|
|
182
|
-
rulesMask: policy.rulesMask,
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
for (const ruleId of Object.values(Rule)) {
|
|
186
|
-
requestPolicy[ruleId] = policy[ruleId];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// handle exclusions
|
|
190
|
-
for (const [inputType, exclusions] of Object.entries(compiled)) {
|
|
191
|
-
for (const e of exclusions) {
|
|
192
|
-
if (!e.matchesUriPath(uriPath)) continue;
|
|
193
|
-
|
|
194
|
-
// url exclusions
|
|
195
|
-
if (inputType === 'url') {
|
|
196
|
-
// if applies to all rules, there is no policy for the request i.e. disable protect
|
|
197
|
-
if (!e.policy) {
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// merge exclusion's policy into the request's policy
|
|
202
|
-
for (const key of Object.keys(e.policy)) {
|
|
203
|
-
const value = e.policy[key];
|
|
204
|
-
if (key === 'rulesMask') {
|
|
205
|
-
// this is how to disable rules bitwise
|
|
206
|
-
requestPolicy.rulesMask = requestPolicy.rulesMask & ~value;
|
|
207
|
-
} else {
|
|
208
|
-
requestPolicy[key] = value;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
} else if (inputType === 'querystring') {
|
|
212
|
-
if (!e.policy) {
|
|
213
|
-
requestPolicy.exclusions.ignoreQuerystring = true;
|
|
214
|
-
} else {
|
|
215
|
-
// merge exclusion's policy into the querystring's policy
|
|
216
|
-
requestPolicy.exclusions.querystringPolicy = requestPolicy.exclusions.querystringPolicy || {};
|
|
217
|
-
for (const key of Object.keys(e.policy)) {
|
|
218
|
-
const value = e.policy[key];
|
|
219
|
-
if (key !== 'rulesMask') {
|
|
220
|
-
requestPolicy.exclusions.querystringPolicy[key] = value;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
} else if (inputType === 'body') {
|
|
225
|
-
if (!e.policy) {
|
|
226
|
-
requestPolicy.exclusions.ignoreBody = true;
|
|
227
|
-
} else {
|
|
228
|
-
// merge exclusion's policy into the querystring's policy
|
|
229
|
-
requestPolicy.exclusions.bodyPolicy = requestPolicy.exclusions.bodyPolicy || {};
|
|
230
|
-
for (const key of Object.keys(e.policy)) {
|
|
231
|
-
const value = e.policy[key];
|
|
232
|
-
if (key !== 'rulesMask') {
|
|
233
|
-
requestPolicy.exclusions.bodyPolicy[key] = value;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
} else {
|
|
238
|
-
// copy matching input exclusions into request policy
|
|
239
|
-
requestPolicy.exclusions[inputType].push(e);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return requestPolicy;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
276
|
function updateGlobalPolicy(remoteSettings) {
|
|
248
277
|
const protectionRules = remoteSettings?.protect?.rules;
|
|
278
|
+
// last updated
|
|
279
|
+
protect.policy.version = Date.now();
|
|
280
|
+
|
|
249
281
|
if (protectionRules) {
|
|
250
282
|
[
|
|
251
283
|
CMD_INJECTION,
|
|
@@ -290,7 +322,8 @@ module.exports = function (core) {
|
|
|
290
322
|
}
|
|
291
323
|
|
|
292
324
|
updateRulesMask();
|
|
293
|
-
protect.policy.exclusions =
|
|
325
|
+
protect.policy.exclusions = processedExclusions;
|
|
326
|
+
|
|
294
327
|
logger.info({ policy: protect.policy }, 'Protect policy updated');
|
|
295
328
|
}
|
|
296
329
|
}
|
|
@@ -302,7 +335,7 @@ module.exports = function (core) {
|
|
|
302
335
|
].filter((exclusion) => exclusion.modes.includes('defend'));
|
|
303
336
|
|
|
304
337
|
if (!exclusions.length) return;
|
|
305
|
-
|
|
338
|
+
processedExclusions = initCompiled();
|
|
306
339
|
|
|
307
340
|
for (const exclusionDtm of exclusions) {
|
|
308
341
|
exclusionDtm.type = exclusionDtm.type || 'URL';
|
|
@@ -310,7 +343,7 @@ module.exports = function (core) {
|
|
|
310
343
|
const { name, protect_rules, urls, type } = exclusionDtm;
|
|
311
344
|
const key = StringPrototypeToLowerCase.call(type);
|
|
312
345
|
|
|
313
|
-
if (!
|
|
346
|
+
if (!processedExclusions[key]) continue;
|
|
314
347
|
|
|
315
348
|
try {
|
|
316
349
|
const e = { name };
|
|
@@ -354,7 +387,7 @@ module.exports = function (core) {
|
|
|
354
387
|
};
|
|
355
388
|
}
|
|
356
389
|
|
|
357
|
-
|
|
390
|
+
processedExclusions[key].push(e);
|
|
358
391
|
} catch (err) {
|
|
359
392
|
logger.error({ err, exclusionDtm }, 'failed to process exclusion');
|
|
360
393
|
}
|
|
@@ -370,5 +403,7 @@ module.exports = function (core) {
|
|
|
370
403
|
|
|
371
404
|
initPolicy();
|
|
372
405
|
|
|
373
|
-
return protect.getPolicy = getPolicy
|
|
406
|
+
return protect.getPolicy = function getPolicy(sourceInfo) {
|
|
407
|
+
return new RequestPolicy(core, sourceInfo);
|
|
408
|
+
};
|
|
374
409
|
};
|
|
@@ -155,7 +155,7 @@ module.exports = function(core) {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
semanticAnalysis.handleCmdInjectionSemanticDangerous = function(sourceContext, sinkContext) {
|
|
158
|
-
const mode = sourceContext.policy
|
|
158
|
+
const mode = sourceContext.policy.getRuleMode(Rule.CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS);
|
|
159
159
|
|
|
160
160
|
if (mode == OFF) return;
|
|
161
161
|
|
|
@@ -167,7 +167,7 @@ module.exports = function(core) {
|
|
|
167
167
|
};
|
|
168
168
|
|
|
169
169
|
semanticAnalysis.handleCmdInjectionSemanticChainedCommands = function(sourceContext, sinkContext) {
|
|
170
|
-
const mode = sourceContext.policy
|
|
170
|
+
const mode = sourceContext.policy.getRuleMode(Rule.CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS);
|
|
171
171
|
|
|
172
172
|
if (mode == OFF) return;
|
|
173
173
|
|
|
@@ -179,10 +179,9 @@ module.exports = function(core) {
|
|
|
179
179
|
};
|
|
180
180
|
|
|
181
181
|
semanticAnalysis.handleCommandInjectionCommandBackdoors = function(sourceContext, sinkContext) {
|
|
182
|
-
const mode = sourceContext.policy
|
|
182
|
+
const mode = sourceContext.policy.getRuleMode(Rule.CMD_INJECTION_COMMAND_BACKDOORS);
|
|
183
183
|
|
|
184
184
|
if (mode == OFF) return;
|
|
185
|
-
|
|
186
185
|
const finding = findBackdoorInjection(sourceContext, sinkContext.value);
|
|
187
186
|
|
|
188
187
|
if (finding) {
|
|
@@ -191,7 +190,7 @@ module.exports = function(core) {
|
|
|
191
190
|
};
|
|
192
191
|
|
|
193
192
|
semanticAnalysis.handlePathTraversalFileSecurityBypass = function(sourceContext, sinkContext) {
|
|
194
|
-
const mode = sourceContext.policy
|
|
193
|
+
const mode = sourceContext.policy.getRuleMode(Rule.PATH_TRAVERSAL_SEMANTIC_FILE_SECURITY_BYPASS);
|
|
195
194
|
|
|
196
195
|
if (mode == OFF) return;
|
|
197
196
|
|
|
@@ -201,7 +200,7 @@ module.exports = function(core) {
|
|
|
201
200
|
};
|
|
202
201
|
|
|
203
202
|
semanticAnalysis.handleXXE = function (sourceContext, sinkContext) {
|
|
204
|
-
const mode = sourceContext.policy
|
|
203
|
+
const mode = sourceContext.policy.getRuleMode(Rule.XXE);
|
|
205
204
|
if (mode == OFF) return;
|
|
206
205
|
|
|
207
206
|
const findings = findExternalEntities(sinkContext.value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/protect",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.68.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)",
|
|
@@ -22,15 +22,15 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@contrast/agent-lib": "^9.1.0",
|
|
24
24
|
"@contrast/common": "1.37.0",
|
|
25
|
-
"@contrast/config": "1.52.
|
|
26
|
-
"@contrast/core": "1.57.
|
|
27
|
-
"@contrast/dep-hooks": "1.26.
|
|
28
|
-
"@contrast/esm-hooks": "2.
|
|
29
|
-
"@contrast/instrumentation": "1.36.
|
|
30
|
-
"@contrast/logger": "1.30.
|
|
31
|
-
"@contrast/patcher": "1.29.
|
|
32
|
-
"@contrast/rewriter": "1.
|
|
33
|
-
"@contrast/scopes": "1.27.
|
|
25
|
+
"@contrast/config": "1.52.1",
|
|
26
|
+
"@contrast/core": "1.57.1",
|
|
27
|
+
"@contrast/dep-hooks": "1.26.1",
|
|
28
|
+
"@contrast/esm-hooks": "2.32.0",
|
|
29
|
+
"@contrast/instrumentation": "1.36.1",
|
|
30
|
+
"@contrast/logger": "1.30.1",
|
|
31
|
+
"@contrast/patcher": "1.29.1",
|
|
32
|
+
"@contrast/rewriter": "1.34.0",
|
|
33
|
+
"@contrast/scopes": "1.27.1",
|
|
34
34
|
"async-hook-domain": "^4.0.1",
|
|
35
35
|
"ipaddr.js": "^2.0.1",
|
|
36
36
|
"on-finished": "^2.4.1",
|