@contrast/agent 4.12.2 → 4.14.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.
Files changed (66) hide show
  1. package/bootstrap.js +2 -3
  2. package/esm.mjs +9 -35
  3. package/lib/assess/membrane/debraner.js +0 -2
  4. package/lib/assess/membrane/index.js +1 -3
  5. package/lib/assess/models/tag-range/util.js +1 -2
  6. package/lib/assess/policy/propagators.json +13 -4
  7. package/lib/assess/policy/rules.json +42 -0
  8. package/lib/assess/policy/signatures.json +18 -0
  9. package/lib/assess/policy/util.js +3 -2
  10. package/lib/assess/propagators/JSON/stringify.js +6 -11
  11. package/lib/assess/propagators/ajv/conditionals.js +0 -3
  12. package/lib/assess/propagators/ajv/json-schema-type-evaluators.js +5 -4
  13. package/lib/assess/propagators/ajv/refs.js +1 -2
  14. package/lib/assess/propagators/ajv/schema-context.js +2 -3
  15. package/lib/assess/propagators/joi/any.js +1 -1
  16. package/lib/assess/propagators/joi/object.js +1 -1
  17. package/lib/assess/propagators/joi/string-base.js +16 -3
  18. package/lib/assess/propagators/mongoose/map.js +1 -1
  19. package/lib/assess/propagators/mongoose/mixed.js +1 -1
  20. package/lib/assess/propagators/mongoose/string.js +1 -1
  21. package/lib/assess/propagators/path/common.js +38 -29
  22. package/lib/assess/propagators/path/resolve.js +1 -0
  23. package/lib/assess/propagators/sequelize/utils.js +1 -2
  24. package/lib/assess/propagators/v8/init-hooks.js +0 -1
  25. package/lib/assess/sinks/dynamo.js +65 -30
  26. package/lib/assess/static/hardcoded.js +3 -3
  27. package/lib/assess/static/read-findings-from-cache.js +40 -0
  28. package/lib/assess/technologies/index.js +12 -13
  29. package/lib/cli-rewriter/index.js +65 -6
  30. package/lib/core/async-storage/hooks/mysql.js +57 -6
  31. package/lib/core/config/options.js +12 -6
  32. package/lib/core/config/util.js +15 -33
  33. package/lib/core/exclusions/input.js +6 -1
  34. package/lib/core/express/index.js +2 -4
  35. package/lib/core/logger/debug-logger.js +2 -2
  36. package/lib/core/stacktrace.js +2 -1
  37. package/lib/hooks/http.js +81 -81
  38. package/lib/hooks/require.js +1 -0
  39. package/lib/instrumentation.js +17 -0
  40. package/lib/protect/analysis/aho-corasick.js +1 -1
  41. package/lib/protect/errors/handler-async-errors.js +66 -0
  42. package/lib/protect/input-analysis.js +7 -13
  43. package/lib/protect/listeners.js +27 -23
  44. package/lib/protect/rules/base-scanner/index.js +2 -2
  45. package/lib/protect/rules/bot-blocker/bot-blocker-rule.js +4 -2
  46. package/lib/protect/rules/cmd-injection/cmdinjection-rule.js +57 -2
  47. package/lib/protect/rules/cmd-injection-semantic-chained-commands/cmd-injection-semantic-chained-commands-rule.js +31 -2
  48. package/lib/protect/rules/cmd-injection-semantic-dangerous-paths/cmd-injection-semantic-dangerous-paths-rule.js +32 -2
  49. package/lib/protect/rules/index.js +42 -21
  50. package/lib/protect/rules/ip-denylist/ip-denylist-rule.js +2 -2
  51. package/lib/protect/rules/nosqli/nosql-injection-rule.js +104 -39
  52. package/lib/protect/rules/path-traversal/path-traversal-rule.js +3 -0
  53. package/lib/protect/rules/rule-factory.js +6 -7
  54. package/lib/protect/rules/signatures/signature.js +3 -0
  55. package/lib/protect/rules/sqli/sql-injection-rule.js +98 -5
  56. package/lib/protect/rules/sqli/sql-scanner/labels.json +0 -3
  57. package/lib/protect/rules/xss/reflected-xss-rule.js +3 -3
  58. package/lib/protect/sample-aggregator.js +65 -57
  59. package/lib/protect/service.js +709 -104
  60. package/lib/reporter/models/app-activity/sample.js +6 -0
  61. package/lib/reporter/speedracer/unknown-connection-state.js +20 -32
  62. package/lib/reporter/translations/to-protobuf/settings/assess-features.js +4 -6
  63. package/lib/reporter/ts-reporter.js +1 -1
  64. package/lib/util/get-file-type.js +43 -0
  65. package/package.json +11 -11
  66. package/perf-logs.js +2 -5
@@ -21,6 +21,7 @@ Copyright: 2022 Contrast Security, Inc
21
21
  */
22
22
 
23
23
  const _ = require('lodash');
24
+
24
25
  const Stream = require('stream');
25
26
  const parseurl = require('parseurl');
26
27
  const agentEmitter = require('../agent-emitter');
@@ -28,25 +29,28 @@ const ApplicationContext = require('./models/application-context');
28
29
  const Request = require('../reporter/models/request');
29
30
  const {
30
31
  AsyncStorage,
31
- KEYS: { SAMPLES, RULES, INPUT_EXCLUSIONS, RES, REQ, REQUEST }
32
+ KEYS: { SAMPLES, RULES, INPUT_EXCLUSIONS, RES, REQ, REQUEST },
32
33
  } = require('../core/async-storage');
33
34
  const Samples = require('./samples.js');
34
35
  const ProtectService = require('./service');
35
36
  const { INPUT_TYPES } = require('../constants');
37
+ const handlerAsyncErrors = require('./errors/handler-async-errors');
36
38
 
37
39
  module.exports = function protectEventListener(agent, reporter) {
38
40
  const service = new ProtectService(agent, reporter);
39
41
 
42
+ handlerAsyncErrors.install();
43
+
40
44
  agentEmitter.on('http.requestStart', (req, res, ipEvent) => {
41
- // We're expected to apply exclusions based only on the url path name,
42
- // not on the full path (which may include query string)
45
+ // apply exclusions based only on the url path name, not on the full
46
+ // path (which may include a query string).
43
47
  const { pathname: urlPath } = parseurl(req);
44
48
 
45
49
  const rules = service.getEnabledRules(urlPath, ipEvent);
46
50
  AsyncStorage.set(RULES, rules);
47
51
  AsyncStorage.set(
48
52
  INPUT_EXCLUSIONS,
49
- service.getEnabledInputExclusions(urlPath)
53
+ service.getEnabledInputExclusions(urlPath),
50
54
  );
51
55
  AsyncStorage.set(SAMPLES, new Samples());
52
56
  });
@@ -65,16 +69,20 @@ module.exports = function protectEventListener(agent, reporter) {
65
69
  }
66
70
 
67
71
  const ctxt = AsyncStorage.getContext();
68
- const rules = sourceRuleFilter(_.get(ctxt, RULES));
69
- const inputExclusions = _.get(ctxt, INPUT_EXCLUSIONS);
70
- const samples = _.get(ctxt, SAMPLES);
71
-
72
- enrichEvent(event, ctxt);
73
- if (agent.config.agent.node.speedracer_input_analysis) {
72
+ if (ctxt.defend) {
73
+ const { exclusions: inputExclusions, samples } = ctxt.defend;
74
+ const rules = sourceRuleFilter(ctxt.defend.rules);
75
+ enrichEvent(event, ctxt);
74
76
  enrichSamples(event, samples);
75
- }
76
77
 
77
- service.handleSourceEvent(event, rules, inputExclusions, samples);
78
+ if (!rules || rules.length === 0) {
79
+ return;
80
+ }
81
+ // it's ugly because this probably should have been here all along as opposed
82
+ // to "enriching" the event with data from ctxt.
83
+ event._ctxt = ctxt;
84
+ service.handleSourceEvent(event, rules, inputExclusions, samples);
85
+ }
78
86
  });
79
87
 
80
88
  agentEmitter.on('protect.sink', (event) => {
@@ -100,13 +108,13 @@ module.exports = function protectEventListener(agent, reporter) {
100
108
  * @param {Object} event the SourceEvent instance
101
109
  * @param {Set} storage async protect storage
102
110
  */
103
- function enrichEvent(event, storageCtxt = AsyncStorage.getContext()) {
111
+ function enrichEvent(event, storageCtxt) {
104
112
  if (storageCtxt) {
105
113
  [
106
114
  { key: '_incomingMessage', ctxtKey: REQ },
107
115
  { key: '_serverResponse', ctxtKey: RES },
108
116
  { key: 'request', ctxtKey: REQUEST },
109
- { key: 'response', ctxtKey: RES }
117
+ { key: 'response', ctxtKey: RES },
110
118
  ].forEach(({ key, ctxtKey }) => {
111
119
  if (!event[key]) {
112
120
  setKey({ storageCtxt, ctxtKey, event, key });
@@ -130,11 +138,8 @@ function enrichEvent(event, storageCtxt = AsyncStorage.getContext()) {
130
138
  * @param {SourceEvent} event
131
139
  * @param {StorageContext} ctxt
132
140
  */
133
- function enrichSamples(event, samples) {
134
- if (event.type !== INPUT_TYPES.URL_PARAMETER) {
135
- return;
136
- }
137
141
 
142
+ function enrichSamples(event, samples) {
138
143
  for (const key of Object.keys(event.data)) {
139
144
  for (const sample of samples.getAllByType(INPUT_TYPES.URL_PARAMETER)) {
140
145
  const decoded = decodeURIComponent(sample.input.name);
@@ -189,14 +194,13 @@ function isStream(data) {
189
194
  }
190
195
 
191
196
  /**
192
- * This filters out which rules should be involved in the handling of source
193
- * events. Under some conditions, like when we are using Speedracer for input
194
- * analysis, we only want a subset of active rules to respond to these events.
197
+ * return a function that filters out rules that speedracer or agent-lib
198
+ * handles.
195
199
  * @param {Object} config The agent configuration
196
- * @returns {Rule[]}
200
+ * @returns {Function} function that returns either filtered or unfiltered rules
197
201
  */
198
202
  function getSourceRuleFilter(config) {
199
- return config.agent.node.speedracer_input_analysis
203
+ return config.agent.node.speedracer_input_analysis && !config.agent.node.native_input_analysis
200
204
  ? (rules) => rules.filter((rule) => rule.inputClassification === false)
201
205
  : (rules) => rules;
202
206
  }
@@ -67,8 +67,8 @@ class BaseScanner {
67
67
  * 1. Parsing the query into a token sequence.
68
68
  * 2. Finding all stop/start indices of the input string within the query.
69
69
  * 3. Comparing these to the indices of the tokens in the sequence.
70
- * @param {String} substring A single input string.
71
- * @param {String} query The query to analize.
70
+ * @param {String} substring An input string.
71
+ * @param {String} query The query to analyze.
72
72
  * @returns {Object[]}
73
73
  */
74
74
  findInjection(substring, query) {
@@ -23,13 +23,15 @@ const { INPUT_TYPES, IMPORTANCE, PROTECTION_MODES } = require('../common');
23
23
  const USER_AGENT = 'user-agent';
24
24
 
25
25
  class BotBlockerRule extends Rule {
26
- constructor(policy = {}) {
27
- super(policy);
26
+ constructor(policy = {}, agent) {
27
+ super(policy, agent);
28
28
  this.id = 'bot-blocker';
29
29
  this.name = 'Bot Blocker';
30
30
  this.blockAtEntry = true;
31
31
  this.mode = PROTECTION_MODES.BLOCK_AT_PERIMETER;
32
32
  this.applicableInputs = [INPUT_TYPES.HEADER];
33
+
34
+ this.usesLibInputAnalysis = true;
33
35
  }
34
36
 
35
37
  /**
@@ -20,10 +20,11 @@ Copyright: 2022 Contrast Security, Inc
20
20
 
21
21
  const Rule = require('../');
22
22
  const { INPUT_TYPES, SINK_TYPES } = require('../common');
23
+ const logger = require('../../../core/logger')('contrast:rules:protect');
23
24
 
24
25
  class CMDInjectionRule extends Rule {
25
- constructor(policy) {
26
- super(policy);
26
+ constructor(policy, agent) {
27
+ super(policy, agent);
27
28
 
28
29
  this.id = 'cmd-injection';
29
30
  this.name = 'Command Injection';
@@ -42,6 +43,45 @@ class CMDInjectionRule extends Rule {
42
43
  INPUT_TYPES.URL_PARAMETER
43
44
  ];
44
45
  this.applicableSinks = [SINK_TYPES.COMMAND];
46
+
47
+ this.usesLibInputAnalysis = true;
48
+ }
49
+
50
+ /**
51
+ * Calls down to the agent analysis library for evaluation with the
52
+ * @param {Samples} applicableSamples Samples cache
53
+ * @param {Set} params.applicableSamples samples applicable to rule id
54
+ */
55
+ evaluateAtSinkForLib({ event, applicableSamples }) {
56
+ if (applicableSamples.size == 0 || !event.data) {
57
+ return;
58
+ }
59
+
60
+ for (const sample of applicableSamples) {
61
+ let evalResult = null;
62
+ try {
63
+ const input = sample.input.value;
64
+ const sinkData = event.data;
65
+ const inputIndex = sinkData.indexOf(input);
66
+
67
+ if (inputIndex !== -1) {
68
+ evalResult = this.agent.agentLib.checkCommandInjectionSink(
69
+ inputIndex,
70
+ input.length,
71
+ input
72
+ );
73
+ }
74
+ } catch (e) {
75
+ logger.info(`Failed to evaluate command-injection sink: ${e}`);
76
+ }
77
+
78
+ if (evalResult) {
79
+ this.appendAttackDetails(sample, evalResult);
80
+ sample.captureAppContext(event);
81
+ logger.warn(`EFFECTIVE - rule: ${this.id}, mode: ${this.mode}`);
82
+ this.blockRequest(sample);
83
+ }
84
+ }
45
85
  }
46
86
 
47
87
  /**
@@ -53,6 +93,21 @@ class CMDInjectionRule extends Rule {
53
93
  buildDetails(sample, findings) {
54
94
  return { command: findings };
55
95
  }
96
+
97
+ /**
98
+ * Builds the details for TS UI rendering based on agent lib findings.
99
+ * @param {Sample} sample relevant protect sample
100
+ * @param {Object} findings The results from the agent library
101
+ * @returns {Object}
102
+ */
103
+ buildDetailsForLib(sample, findings) {
104
+ return {
105
+ command: sample.input.value.substring(
106
+ findings.startIndex,
107
+ findings.endIndex
108
+ )
109
+ };
110
+ }
56
111
  }
57
112
 
58
113
  module.exports = CMDInjectionRule;
@@ -24,8 +24,8 @@ const UserInputFactory = require('../../../reporter/models/utils/user-input-fact
24
24
  const ChainedCommandScanner = require('./chained-command-scanner');
25
25
 
26
26
  class CMDInjectionSemanticChainedCommandsRule extends Rule {
27
- constructor(policy) {
28
- super(policy);
27
+ constructor(policy, agent) {
28
+ super(policy, agent);
29
29
  this.id = 'cmd-injection-semantic-chained-commands';
30
30
  this.name = 'Command Injection Chained Commands';
31
31
  this.applicableSinks = [SINK_TYPES.COMMAND];
@@ -72,6 +72,35 @@ class CMDInjectionSemanticChainedCommandsRule extends Rule {
72
72
  }
73
73
  }
74
74
 
75
+ /**
76
+ * Performs semantic analysis to determine if there are chained subcommands
77
+ * using agent-lib
78
+ * @param {ApplicationContext} event Sink event
79
+ * @param {Request} request Request model
80
+ * @param {Samples} samples Samples cache
81
+ */
82
+ evaluateAtSinkForLib({ event, request, samples }) {
83
+ const { data: command } = event;
84
+ const index = this.agent.agentLib.indexOfChaining(command);
85
+ if (index != -1) {
86
+ const sample = this.createAndSaveSample({
87
+ input: UserInputFactory.makeOne({ value: command }),
88
+ attributes: { effective: true },
89
+ classification: IMPORTANCE.DEFINITE,
90
+ event,
91
+ samples,
92
+ request
93
+ });
94
+
95
+ this.appendAttackDetails(sample, {
96
+ command,
97
+ findings: [CMD_INJECTION_SEMANTIC_TYPES.CHAINING]
98
+ });
99
+
100
+ this.blockRequest(sample);
101
+ }
102
+ }
103
+
75
104
  /**
76
105
  * Builds the details for TS UI rendering.
77
106
  * @param {Sample} sample N/a in this rule's case
@@ -24,8 +24,8 @@ const UserInputFactory = require('../../../reporter/models/utils/user-input-fact
24
24
  const DangerousPathsScanner = require('./dangerous-paths-scanner');
25
25
 
26
26
  class CMDInjectionSemanticDangerousPathsRule extends Rule {
27
- constructor(policy) {
28
- super(policy);
27
+ constructor(policy, agent) {
28
+ super(policy, agent);
29
29
  this.id = 'cmd-injection-semantic-dangerous-paths';
30
30
  this.name = 'Command Injection Dangerous Paths';
31
31
  this.applicableSinks = [SINK_TYPES.COMMAND];
@@ -66,6 +66,36 @@ class CMDInjectionSemanticDangerousPathsRule extends Rule {
66
66
  }
67
67
  }
68
68
 
69
+ /**
70
+ * Check for dangerous paths in the input command using the agent library.
71
+ * @param {ApplicationContext} event Sink event
72
+ * @param {Request} request Request model
73
+ * @param {Samples} samples Samples cache
74
+ */
75
+ evaluateAtSinkForLib({ event, request, samples }) {
76
+ const { data: command } = event;
77
+ const containsDangerousPath = this.agent.agentLib.containsDangerousPath(
78
+ command
79
+ );
80
+ if (containsDangerousPath) {
81
+ const sample = this.createAndSaveSample({
82
+ input: UserInputFactory.makeOne({ value: command }),
83
+ attributes: { effective: true },
84
+ classification: IMPORTANCE.DEFINITE,
85
+ event,
86
+ samples,
87
+ request
88
+ });
89
+
90
+ this.appendAttackDetails(sample, {
91
+ command,
92
+ findings: [CMD_INJECTION_SEMANTIC_TYPES.PATH_ARGUMENT]
93
+ });
94
+
95
+ this.blockRequest(sample);
96
+ }
97
+ }
98
+
69
99
  /**
70
100
  * Builds the details for TS UI rendering.
71
101
  * @param {Sample} sample N/a in this rule's case
@@ -28,13 +28,37 @@ const {
28
28
  } = require('./common');
29
29
 
30
30
  class Rule {
31
- constructor(policy = { mode: NO_ACTION }) {
31
+ /**
32
+ * Rules that need to make use of the agent lib can also take
33
+ * a second argument: `agent' which is a reference to the agent object.
34
+ */
35
+ constructor(policy, agent) {
36
+ if (!policy) {
37
+ policy = { mode: NO_ACTION };
38
+ }
32
39
  this.policy = policy;
33
40
 
34
41
  this.blockAtEntry = policy.mode === BLOCK_AT_PERIMETER;
35
42
  this.id = policy.id;
36
43
  this.mode = policy.mode;
37
44
  this.name = policy.name;
45
+ this.applicableInputs = [];
46
+ this.applicableSinks = [];
47
+ this.agent = agent;
48
+ const config = (this.agent && this.agent.config) || { agent: { node: {} } };
49
+
50
+ this.agentLibEnabled =
51
+ config.agent.node.native_input_analysis &&
52
+ config.agent.node.speedracer_input_analysis;
53
+
54
+ if (this.agentLibEnabled) {
55
+ this.evaluator = null;
56
+ }
57
+
58
+ // override this value if the rule uses agent library input analysis.
59
+ // all rules that agentLibBit applies to use this field with the single
60
+ // exception of nosqli which uses node input analysis.
61
+ this.usesLibInputAnalysis = false;
38
62
 
39
63
  // Samples --> {}
40
64
  // This lets us manage non-sample state for each rule. This
@@ -66,10 +90,7 @@ class Rule {
66
90
  * @returns {Boolean}
67
91
  */
68
92
  appliesToInputType(type, name) {
69
- return (
70
- _.isEmpty(this.applicableInputs) ||
71
- _.includes(this.applicableInputs, type)
72
- );
93
+ return this.applicableInputs.length === 0 || this.applicableInputs.includes(type);
73
94
  }
74
95
 
75
96
  /**
@@ -79,11 +100,7 @@ class Rule {
79
100
  * @returns {Boolean}
80
101
  */
81
102
  appliesToSink(type) {
82
- if (!this.applicableSinks) {
83
- return false;
84
- }
85
-
86
- return _.includes(this.applicableSinks, type);
103
+ return this.applicableSinks.includes(type);
87
104
  }
88
105
 
89
106
  /**
@@ -105,12 +122,6 @@ class Rule {
105
122
  return this.signature;
106
123
  }
107
124
 
108
- if (!this.evaluator) {
109
- throw new Error(
110
- `Rule ${this.id} has no custom evaluator for signature-matching/classification`
111
- );
112
- }
113
-
114
125
  return this.evaluator;
115
126
  }
116
127
 
@@ -144,6 +155,7 @@ class Rule {
144
155
  const name = sample.input ? sample.input.name : '<anon>';
145
156
  const type = sample.input ? sample.input.type : '<no-type>';
146
157
 
158
+ // TODO: should this be ||?
147
159
  if (!this.blockAtEntry && !sample.effective) {
148
160
  return;
149
161
  }
@@ -172,6 +184,10 @@ class Rule {
172
184
  preFilterUserInput(input, event, samples) {
173
185
  const evaluator = this.getEvaluator();
174
186
 
187
+ if (!evaluator) {
188
+ return;
189
+ }
190
+
175
191
  // This is to support the newer API accepting a UserInput,
176
192
  let evaluation;
177
193
  if (this.signature) {
@@ -188,7 +204,7 @@ class Rule {
188
204
  samples
189
205
  });
190
206
 
191
- if (_.get(sample, 'confirmedAttack') === true) {
207
+ if (sample && sample.confirmedAttack === true && this.mode === BLOCK_AT_PERIMETER) {
192
208
  this.blockRequest(sample);
193
209
  }
194
210
  }
@@ -247,7 +263,7 @@ class Rule {
247
263
 
248
264
  logger.debug(`${classifiedAs}: ${id} - ${type}.${name}.${value}`);
249
265
  if (classifiedAs === IMPORTANCE.NONE) {
250
- return;
266
+ return null;
251
267
  }
252
268
 
253
269
  const sample = samples.addRuleSample({
@@ -277,8 +293,8 @@ class Rule {
277
293
  // for CEF extensions
278
294
  outcome: this.getAttackStatus(sample),
279
295
  pri: this.id,
280
- request: _.get(sample, 'request.uri', '').replace(/ /g, '<space>'),
281
- spt: _.get(sample, 'request.port', ''),
296
+ request: (sample.request.uri || '').replace(/ /g, '<space>'),
297
+ spt: sample.request.port || '',
282
298
  requestMethod: sample.request.method,
283
299
  src: sample.request.ip,
284
300
  value: sample.input.value
@@ -329,7 +345,12 @@ class Rule {
329
345
  */
330
346
  appendAttackDetails(sample, findings) {
331
347
  sample.effective = true;
332
- sample.details = this.buildDetails(sample, findings);
348
+
349
+ if (!(this.usesLibInputAnalysis && this.agentLibEnabled) || !this.buildDetailsForLib) {
350
+ sample.details = this.buildDetails(sample, findings);
351
+ } else {
352
+ sample.details = this.buildDetailsForLib(sample, findings);
353
+ }
333
354
  }
334
355
  }
335
356
 
@@ -26,8 +26,8 @@ const {
26
26
  const Rule = require('../');
27
27
 
28
28
  class IpDenylistRule extends Rule {
29
- constructor(policy) {
30
- super(policy);
29
+ constructor(policy, agent) {
30
+ super(policy, agent);
31
31
  this._analyzer = new IpAnalyzer(policy.data);
32
32
  // TODO: This should go away with CONTRAST-34184
33
33
  this._analyzer.on('expired', (expired) => {
@@ -13,8 +13,8 @@ Copyright: 2022 Contrast Security, Inc
13
13
  way not consistent with the End User License Agreement.
14
14
  */
15
15
  'use strict';
16
- /* eslint-disable complexity */
17
16
  const _ = require('lodash');
17
+ const util = require('util');
18
18
 
19
19
  const logger = require('../../../core/logger')('contrast:rules:protect');
20
20
  const { INPUT_TYPES, SINK_TYPES } = require('../common');
@@ -32,12 +32,12 @@ const ScannerKit = new Map([
32
32
  [MONGODB, () => require('../nosqli/nosql-scanner').create('MongoDB')],
33
33
  [RETHINKDB, () => require('../nosqli/nosql-scanner').create('RethinkDB')]
34
34
  ]);
35
- const SubstringFinder = require('../base-scanner/substring-finder');
36
35
 
37
36
  class NoSqlInjectionRule extends require('../') {
38
- constructor(policy = {}) {
37
+ // eslint-disable-next-line default-param-last
38
+ constructor(policy = {}, agent) {
39
39
  policy.inputParseDepth = 3;
40
- super(policy);
40
+ super(policy, agent);
41
41
 
42
42
  this._scanners = new Map();
43
43
 
@@ -52,56 +52,65 @@ class NoSqlInjectionRule extends require('../') {
52
52
  INPUT_TYPES.QUERYSTRING,
53
53
  INPUT_TYPES.XML_VALUE,
54
54
  INPUT_TYPES.URI,
55
- INPUT_TYPES.URL_PARAMETER
55
+ INPUT_TYPES.URL_PARAMETER,
56
+ INPUT_TYPES.BODY,
57
+ INPUT_TYPES.MULTIPART_VALUE,
56
58
  ];
59
+
60
+ // POST bodies are handled by the lib.
61
+ // if lib is disabled, node input analysis needs to handle this.
62
+ if (!this.agentLibEnabled) {
63
+ this.applicableInputs.push(INPUT_TYPES.BODY);
64
+ }
65
+
57
66
  this.applicableSinks = [SINK_TYPES.NOSQL_QUERY];
67
+
68
+ // explicitly set to false so that this rule will continue to use node input analysis.
69
+ this.usesLibInputAnalysis = false;
58
70
  }
59
71
 
72
+ /**
73
+ * Evaluates the sink data by scanning the document object for repeated instances
74
+ * of the saved input object/strings. This applies to both string injection and
75
+ * object expansion.
76
+ * @param {SinkEvent} event Emitted by database drivers/wrappers/ORMs
77
+ * @param {Set} applicableSamples set of Samples applicable to rule.
78
+ */
79
+ // eslint-disable-next-line complexity
60
80
  evaluateAtSink({ event, applicableSamples }) {
61
- if (_.isEmpty(applicableSamples) || !event.data) {
81
+ if (applicableSamples.size == 0 || !event.data) {
62
82
  return;
63
83
  }
64
84
 
65
- if (typeof event.data === 'object') {
85
+ const { data } = event;
86
+ if (typeof data === 'object') {
66
87
  for (const sample of applicableSamples) {
67
- const requestData = this.getRequestData(sample.input);
68
- if (!requestData) {
88
+ const doc = this.getInput(sample);
89
+ if (!doc) {
69
90
  return;
70
91
  }
71
- let found;
72
- traverse(event.data, (key, value) => {
73
- if (found) {
92
+
93
+ let evalResult = null;
94
+ traverse(data, (key, value) => {
95
+ if (evalResult) {
74
96
  return;
75
97
  }
76
98
 
77
- Object.keys(requestData).some((reqKey) => {
78
- if (value[reqKey] === requestData[reqKey]) {
79
- found = reqKey;
80
- return true;
99
+ // check for _exact_ match under clause for saved user input
100
+ for (const queryClause of Object.keys(doc)) {
101
+ if (key === queryClause) {
102
+ if (_.isEqual(value, doc[queryClause])) {
103
+ evalResult = this.buildFinding(data, queryClause, doc);
104
+ }
81
105
  }
82
- });
106
+ }
83
107
  });
84
108
 
85
- if (found) {
86
- const query = require('util').inspect(event.data, false, null);
87
-
88
- let injection;
89
- for (const location of new SubstringFinder(query, found)) {
90
- if (location) {
91
- injection = {
92
- input: found,
93
- location,
94
- query
95
- };
96
- break;
97
- }
98
- }
99
- if (injection) {
100
- this.appendAttackDetails(sample, injection);
101
- sample.captureAppContext(event);
102
- logger.warn(`EFFECTIVE - rule: ${this.id}, mode: ${this.mode}`);
103
- this.blockRequest(sample);
104
- }
109
+ if (evalResult) {
110
+ this.appendAttackDetails(sample, evalResult);
111
+ sample.captureAppContext(event);
112
+ logger.warn(`EFFECTIVE - rule: ${this.id}, mode: ${this.mode}`);
113
+ this.blockRequest(sample);
105
114
  }
106
115
  }
107
116
  } else if (typeof event.data === 'string' || event.data instanceof String) {
@@ -120,6 +129,47 @@ class NoSqlInjectionRule extends require('../') {
120
129
  }
121
130
  }
122
131
 
132
+ /**
133
+ * Depending on the sample's data, collect the query clause and object
134
+ * for a given input sample. Handles both body inputs and other types.
135
+ * If the library is enabled and the input is a JSON body, it will parse
136
+ * query clauses out and pass them in _inputInfoForSink.
137
+ * @param {Sample} sample applicable sample for nosqli
138
+ * @returns {Object} relevant query clause and doc object to search for.
139
+ */
140
+ getInput(sample) {
141
+ if (!_.isEmpty(sample._inputInfoForSink)) {
142
+ return {
143
+ [sample._inputInfoForSink.queryClause]: sample._inputInfoForSink.docObject
144
+ };
145
+ }
146
+
147
+ const reqData = this.getRequestData(sample.input);
148
+
149
+ return reqData;
150
+ }
151
+
152
+ /**
153
+ * Build the query object based on the inputs passed and stringify them for reporting.
154
+ * @param {String} queryClause the mongo query clause (ex: $ne, $where, etc.)
155
+ * @param {Object} docObject the document object passed to the mongo function.
156
+ */
157
+ buildFinding(queryObject, queryClause, docObject) {
158
+ // this input string being generated is kinda duplicate work from the input stage
159
+ // but it is only really hit when a nosqli is found, so I'm not sure its a big deal.
160
+ const input = util.inspect(docObject, false, null);
161
+ const query = util.inspect(queryObject, false, null);
162
+ // this substring being generated is some duplicate work.
163
+ const start = query.indexOf(input);
164
+ return {
165
+ input,
166
+ // account for query clause, ':' and ' '.
167
+ start: start - queryClause.length - 2,
168
+ end: start + (queryClause.length + 2),
169
+ query
170
+ };
171
+ }
172
+
123
173
  /**
124
174
  * Given the sample's user input object, will read the value from the request
125
175
  * from the async storage context.
@@ -149,7 +199,7 @@ class NoSqlInjectionRule extends require('../') {
149
199
  this.buildPathArray(_documentPath, pathArray);
150
200
  } else {
151
201
  // only qs params and body are "expandable"
152
- pathArray.push('body', ..._documentPath.split('.'));
202
+ pathArray.push('query', ..._documentPath.split('.'));
153
203
  }
154
204
 
155
205
  let data;
@@ -213,9 +263,24 @@ class NoSqlInjectionRule extends require('../') {
213
263
  return null;
214
264
  }
215
265
 
266
+ // if it's not the old node format, then it's the agent-lib format.
267
+ // yeah, i know it's ugly.
268
+ if (!findings.boundary) {
269
+ const { start, end, query } = findings;
270
+ return {
271
+ start,
272
+ end,
273
+ // since there are no actual `token boundary overruns' in nosqli,
274
+ // are these not the exact same as start and end?
275
+ boundaryOverrunIndex: end,
276
+ inputBoundaryIndex: start,
277
+ query
278
+ };
279
+ }
280
+
216
281
  const { boundary, location, query } = findings;
217
282
  let inputBoundaryIndex, boundaryOverrunIndex;
218
- const start = location[0];
283
+ const start = location[0]; // eslint-disable-line
219
284
  const end = location[1] + 1;
220
285
 
221
286
  if (boundary) {