@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.
@@ -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 = captureStacktrace(
47
- { name: `${name}.${method}`, value },
48
- { constructorOpt: hooked, prependFrames: [orig] },
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 { BLOCKING_MODES, simpleTraverse, Rule, isString } = require('@contrast/common');
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.virtualPatches;
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 { protect: { agentLib, inputTracing, throwSecurityException } } = core;
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 = captureStacktrace(
47
- { name, value },
48
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
49
- { name: 'eval', value },
50
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
105
- { name, value },
106
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
50
- { name: 'Function', value: fnBody },
51
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
48
- { name, value },
49
- { constructorOpt: data.hooked }
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 = captureStacktrace(
71
- { name, value },
72
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
95
- { name, value },
96
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
136
- { name, value },
137
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
154
- { name, value },
155
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
178
- { name, value },
179
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
197
- { name, value },
198
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
60
- { name, value },
61
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
44
- { name, value },
45
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
53
- { name, value },
54
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
48
- { name, value },
49
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
50
- { name, value: codeString },
51
- { constructorOpt: hooked, prependFrames: [orig] }
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)) ? captureStacktrace(
74
- { name: 'vm.runInNewContext', value: codeString },
75
- { constructorOpt: hooked, prependFrames: [orig] }
76
- ) : null;
77
- const envObjSinkContext = isNonEmptyObject(envObj) ? captureStacktrace(
78
- { name: 'vm.runInNewContext', value: envObj },
79
- { constructorOpt: hooked, prependFrames: [orig] }
80
- ) : null;
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 = captureStacktrace(
100
- { name: 'vm.createContext', value: envObj },
101
- { constructorOpt: hooked, prependFrames: [orig] }
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 = captureStacktrace(
120
- { name: 'vm.Script.prototype.runInNewContext', value: envObj },
121
- { constructorOpt: hooked, prependFrames: [orig] }
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 { protect } = core;
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
- const ix = req.url.indexOf('?');
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: protect.getPolicy(),
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 { config, logger, messages, protect } = core;
33
- const policy = protect.policy = {};
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 'off';
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
- for (const [ruleId, mode] of Object.entries(policy)) {
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
- return { ...policy };
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
- initPolicy();
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
- messages.on(SERVER_SETTINGS_UPDATE, (remoteSettings) => {
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: sinkContext.value },
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 = captureStacktrace(
64
- { name: 'libxmljs.parseXmlString', value },
65
- { constructorOpt: hooked, prependFrames: [orig] }
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.7.0",
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.4",
22
- "@contrast/core": "1.7.0",
23
- "@contrast/esm-hooks": "1.3.0",
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"