@contrast/protect 1.74.0 → 1.75.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.
@@ -20,6 +20,7 @@ const {
20
20
  BLOCKING_MODES,
21
21
  Rule,
22
22
  ProtectRuleMode: { OFF, MONITOR },
23
+ identity,
23
24
  isString,
24
25
  traverseKeysAndValues,
25
26
  traverseValues,
@@ -28,7 +29,7 @@ const {
28
29
  ArrayPrototypeJoin,
29
30
  ArrayPrototypeSlice,
30
31
  StringPrototypeToLowerCase,
31
- StringPrototypeSplit,
32
+ StringPrototypeMatch,
32
33
  }
33
34
  } = require('@contrast/common');
34
35
  const { Core } = require('@contrast/core/lib/ioc/core');
@@ -52,16 +53,13 @@ const { Core } = require('@contrast/core/lib/ioc/core');
52
53
  // input and make a decision immediately (i.e., there is no sink). so these two
53
54
  // rules will need to be implemented by the agent and called from these handlers.
54
55
 
55
-
56
56
  // agent-lib treats each type of nosql database as a separate
57
57
  // rule. they are all mapped to 'nosql-injection' in the agent.
58
58
  // maybe this will need to change.
59
59
  const agentLibRuleTypeToName = {
60
60
  'nosql-injection-mongo': 'nosql-injection'
61
61
  };
62
-
63
62
  const preferWW = { preferWorthWatching: true };
64
-
65
63
  const acceptedMethods = new Set([
66
64
  'acl',
67
65
  'baseline-control',
@@ -95,9 +93,11 @@ const acceptedMethods = new Set([
95
93
  'update',
96
94
  'version-control',
97
95
  ]);
96
+ const componentName = 'protect.inputAnalysis.handlers';
97
+ const noInstrumentationStore = { lock: true, name: componentName };
98
98
 
99
99
  module.exports = Core.makeComponent({
100
- name: 'protect.inputAnalysis.handlers',
100
+ name: componentName,
101
101
  factory(core) {
102
102
  const {
103
103
  logger,
@@ -106,6 +106,7 @@ module.exports = Core.makeComponent({
106
106
  agentLib,
107
107
  inputAnalysis,
108
108
  },
109
+ scopes,
109
110
  config,
110
111
  } = core;
111
112
 
@@ -676,41 +677,48 @@ module.exports = Core.makeComponent({
676
677
  const findingsForScoreAtom = {};
677
678
  const valueToResultByRuleId = {};
678
679
 
679
- Object.values(resultsMap).forEach(resultsByRuleId => {
680
- resultsByRuleId.forEach(resultByRuleId => {
681
- const {
682
- ruleId,
683
- exploited,
684
- score,
685
- value,
686
- key,
687
- inputType
688
- } = resultByRuleId;
680
+ const flattened = Object.values(resultsMap).flatMap(identity);
681
+ for (const result of flattened) {
682
+ const {
683
+ ruleId,
684
+ exploited,
685
+ score,
686
+ value,
687
+ key,
688
+ inputType
689
+ } = result;
690
+ if (
691
+ sourceContext.policy.getRuleMode(ruleId) !== MONITOR ||
692
+ exploited === true ||
693
+ score >= 90 ||
694
+ !probesRules.some((rule) => rule === ruleId) ||
695
+ inputType == InputType.UNKNOWN ||
696
+ flattened.some((maybeReported) => (
697
+ // remove chances of duplicate analysis for "similar" findings that
698
+ // would have already been reported for being blocked or exploited
699
+ maybeReported.value == result.value &&
700
+ maybeReported.inputType == result.inputType &&
701
+ maybeReported.key == result.key &&
702
+ (maybeReported.exploited || maybeReported.blocked)
703
+ ))
704
+ ) {
705
+ continue;
706
+ }
689
707
 
690
- if (
691
- sourceContext.policy.getRuleMode(ruleId) !== MONITOR ||
692
- exploited === true || // todo: remove
693
- score >= 90 ||
694
- !probesRules.some((rule) => rule === ruleId) ||
695
- inputType == InputType.UNKNOWN
696
- ) {
697
- return;
708
+ const dataType = findingsForScoreRequest[inputType];
709
+ if (!dataType) {
710
+ if (!findingsForScoreAtom[value]) {
711
+ findingsForScoreAtom[value] = {};
698
712
  }
699
713
 
700
- const dataType = findingsForScoreRequest[inputType];
701
- if (!dataType) {
702
- if (!findingsForScoreAtom[value]) {
703
- findingsForScoreAtom[value] = {};
704
- }
714
+ findingsForScoreAtom[value][inputType] = result;
715
+ continue;
716
+ }
705
717
 
706
- findingsForScoreAtom[value][inputType] = resultByRuleId;
707
- return;
708
- }
718
+ dataType[key] = value;
719
+ valueToResultByRuleId[value] = result;
720
+ }
709
721
 
710
- dataType[key] = value;
711
- valueToResultByRuleId[value] = resultByRuleId;
712
- });
713
- });
714
722
  const { ParameterValue, HeaderValue, CookieValue } = findingsForScoreRequest;
715
723
  const results =
716
724
  agentLib.scoreRequestConnect(
@@ -938,12 +946,21 @@ module.exports = Core.makeComponent({
938
946
  function ipListAnalysis(reqIp, reqHeaders, list) {
939
947
  const forwardedIps = [];
940
948
 
941
- for (let i = 0; i < reqHeaders.length; i++) {
942
- if (reqHeaders[i] === 'x-forwarded-for') {
943
- const ipsFromHeaders = StringPrototypeSplit.call(reqHeaders[i + 1], /[,;]+/);
944
- forwardedIps.push(...ipsFromHeaders);
949
+ // RegExp.exec will still get called under the hood even with using primordials
950
+ scopes.instrumentation.run(noInstrumentationStore, () => {
951
+ for (let i = 0; i < reqHeaders.length; i++) {
952
+ if (reqHeaders[i] === 'x-forwarded-for') {
953
+ // This matches one or more valid IPv4/IPv6 characters, automatically
954
+ // skipping delimiters, whitespace, and invalid characters
955
+ const matches = StringPrototypeMatch.call(reqHeaders[i + 1], /[0-9a-fA-F.:]+/g);
956
+ if (matches) {
957
+ for (const ip of matches) {
958
+ forwardedIps.push(ip);
959
+ }
960
+ }
961
+ }
945
962
  }
946
- }
963
+ });
947
964
 
948
965
  const ipsToCheck = [reqIp, ...forwardedIps];
949
966
  const now = new Date().getTime();
@@ -960,7 +977,7 @@ module.exports = Core.makeComponent({
960
977
 
961
978
  // Ignore bad IP values.
962
979
  if (!address.isValid(currentIp)) {
963
- logger.warn('Unable to parse %s.', currentIp);
980
+ logger.debug({ value: currentIp, length: currentIp.length }, 'Unable to parse IP address %s.', currentIp);
964
981
  continue;
965
982
  }
966
983
  const expired = doesExpire ? expiresAt - now <= 0 : false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/protect",
3
- "version": "1.74.0",
3
+ "version": "1.75.0",
4
4
  "description": "Contrast service providing framework-agnostic Protect support",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -20,18 +20,18 @@
20
20
  "test": "bash ../scripts/test.sh"
21
21
  },
22
22
  "dependencies": {
23
- "@contrast/agent-lib": "^9.1.0",
24
- "@contrast/common": "1.41.0",
25
- "@contrast/config": "1.57.0",
26
- "@contrast/core": "1.62.0",
27
- "@contrast/dep-hooks": "1.31.0",
28
- "@contrast/esm-hooks": "2.37.0",
29
- "@contrast/instrumentation": "1.41.0",
30
- "@contrast/logger": "1.35.0",
31
- "@contrast/patcher": "1.34.0",
32
- "@contrast/rewriter": "1.39.0",
33
- "@contrast/scopes": "1.32.0",
34
- "@contrast/stack-trace-factory": "1.2.0",
23
+ "@contrast/agent-lib": "9.2.0",
24
+ "@contrast/common": "1.41.1",
25
+ "@contrast/config": "1.58.0",
26
+ "@contrast/core": "1.63.0",
27
+ "@contrast/dep-hooks": "1.32.0",
28
+ "@contrast/esm-hooks": "2.38.0",
29
+ "@contrast/instrumentation": "1.42.0",
30
+ "@contrast/logger": "1.36.0",
31
+ "@contrast/patcher": "1.35.0",
32
+ "@contrast/rewriter": "1.40.0",
33
+ "@contrast/scopes": "1.33.0",
34
+ "@contrast/stack-trace-factory": "1.3.0",
35
35
  "async-hook-domain": "^4.0.1",
36
36
  "ipaddr.js": "^2.0.1",
37
37
  "on-finished": "^2.4.1",