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