@contrast/assess 1.27.1 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/constants.js CHANGED
@@ -16,9 +16,9 @@
16
16
  'use strict';
17
17
 
18
18
  const InstrumentationType = {
19
- SOURCE: 'source',
20
- PROPAGATOR: 'propagator',
21
- RULE: 'rule',
19
+ SOURCE: 'SOURCE',
20
+ PROPAGATOR: 'PROPAGATOR',
21
+ RULE: 'RULE',
22
22
  };
23
23
 
24
24
  module.exports = {
@@ -15,17 +15,16 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const {
19
- createAppendTags
20
- } = require('../../tag-utils');
21
- const { patchType } = require('../common');
22
18
  const { isString, join, inspect } = require('@contrast/common');
19
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../constants');
20
+ const { createAppendTags } = require('../../tag-utils');
21
+ const { patchType } = require('../common');
23
22
 
24
23
  module.exports = function(core) {
25
24
  const {
26
- scopes: { sources, instrumentation },
27
25
  patcher,
28
26
  assess: {
27
+ getSourceContext,
29
28
  eventFactory: { createPropagationEvent },
30
29
  dataflow: { tracker }
31
30
  }
@@ -71,7 +70,7 @@ module.exports = function(core) {
71
70
  patchType,
72
71
  post(data) {
73
72
  const { args: origArgs, obj, result, hooked, orig } = data;
74
- if (!result || !sources.getStore()?.assess || instrumentation.isLocked()) return;
73
+ if (!result || !(getSourceContext(PROPAGATOR))) return;
75
74
 
76
75
  const resultInfo = tracker.getData(result);
77
76
  const delimiter = origArgs[0] === undefined ? ',' : origArgs[0];
@@ -14,11 +14,13 @@
14
14
  */
15
15
  'use strict';
16
16
 
17
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../constants');
17
18
  const { patchType } = require('../common');
18
19
 
19
20
  module.exports = function(core) {
20
21
  const {
21
22
  assess: {
23
+ getSourceContext,
22
24
  eventFactory,
23
25
  dataflow: { tracker }
24
26
  },
@@ -35,7 +37,7 @@ module.exports = function(core) {
35
37
  post(data) {
36
38
  const { hooked, obj, orig, result } = data;
37
39
 
38
- if (!result) return;
40
+ if (!result || !getSourceContext(PROPAGATOR)) return;
39
41
 
40
42
  const bufferInfo = tracker.getData(obj);
41
43
  if (!bufferInfo) {
@@ -16,102 +16,105 @@
16
16
  'use strict';
17
17
 
18
18
  const util = require('util');
19
- const {
20
- createAppendTags
21
- } = require('../../../tag-utils');
22
- const { patchType } = require('../../common');
19
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
20
+ const { createAppendTags } = require('../../../tag-utils');
23
21
 
24
22
  module.exports = function(core) {
25
23
  const {
26
- scopes: { instrumentation, sources },
27
24
  patcher,
28
25
  assess: {
26
+ getSourceContext,
29
27
  eventFactory: { createPropagationEvent },
30
28
  dataflow: { tracker }
31
29
  }
32
30
  } = core;
33
31
 
34
32
  const inspect = patcher.unwrap(util.inspect);
33
+ const origSym = Symbol('ContrastMethods.add.orig');
35
34
 
36
35
  return core.assess.dataflow.propagation.contrastMethodsInstrumentation.add = {
37
36
  install() {
38
- patcher.patch(global.ContrastMethods, 'add', {
39
- name: 'ContrastMethods.add',
40
- patchType,
41
- post(data) {
42
- const { args, result, hooked } = data;
43
- if (!result || !sources.getStore()?.assess || instrumentation.isLocked()) return;
37
+ // + is fast and typically called often. therefore we patch ContrastMethods.add
38
+ // manually instead of using patcher. this propagator is the only
39
+ // patch for it, so we don't have to worry about managing patch execution order
40
+ // (which patcher would do).
41
+ const { add } = global.ContrastMethods;
42
+ global.ContrastMethods.add = function(...args) {
43
+ // first get result, then following logic acts as post-hook in patcher speak
44
+ const result = add(...args);
44
45
 
45
- const rInfo = tracker.getData(result);
46
- if (rInfo) {
47
- // this may happen w/ '' + 'tracked' => 'tracked'
48
- return;
49
- }
46
+ if (!result || !getSourceContext(PROPAGATOR)) return result;
50
47
 
51
- const leftStringInfo = tracker.getData(args[0]);
52
- const rightStringInfo = tracker.getData(args[1]);
48
+ const rInfo = tracker.getData(result);
49
+ if (rInfo) {
50
+ // this may happen w/ '' + 'tracked' => 'tracked'
51
+ return result;
52
+ }
53
53
 
54
- let newTags = {};
55
- const history = [];
54
+ const leftStringInfo = tracker.getData(args[0]);
55
+ const rightStringInfo = tracker.getData(args[1]);
56
56
 
57
- if (leftStringInfo) {
58
- history.push(leftStringInfo);
59
- newTags = leftStringInfo.tags || {};
60
- }
57
+ let newTags = {};
58
+ const history = [];
61
59
 
62
- if (rightStringInfo) {
63
- history.push(rightStringInfo);
64
- newTags = createAppendTags(newTags, rightStringInfo.tags, args[0].length);
65
- }
60
+ if (leftStringInfo) {
61
+ history.push(leftStringInfo);
62
+ newTags = leftStringInfo.tags || {};
63
+ }
66
64
 
67
- if (history.length) {
68
- const leftArg = leftStringInfo ? leftStringInfo.value : args[0];
69
- const rightArg = rightStringInfo ? rightStringInfo.value : args[1];
70
- const event = createPropagationEvent({
71
- args: [
72
- {
73
- tracked: !!leftStringInfo,
74
- value: leftArg
75
- },
76
- {
77
- tracked: !!rightStringInfo,
78
- value: rightArg,
79
- }
80
- ],
81
- context: `${inspect(leftArg)} + ${inspect(rightArg)}`,
82
- moduleName: 'global',
83
- methodName: 'ContrastMethods.add',
84
- history,
85
- object: {
86
- value: 'String Addition',
87
- tracked: false
88
- },
89
- name: 'ContrastMethods.add',
90
- result: {
91
- value: result,
92
- tracked: true
93
- },
94
- source: 'P',
95
- stacktraceOpts: {
96
- constructorOpt: hooked,
97
- },
98
- tags: newTags,
99
- target: 'R',
100
- });
65
+ if (rightStringInfo) {
66
+ history.push(rightStringInfo);
67
+ newTags = createAppendTags(newTags, rightStringInfo.tags, args[0].length);
68
+ }
101
69
 
102
- if (!event) return;
70
+ if (history.length) {
71
+ const leftArg = leftStringInfo ? leftStringInfo.value : args[0];
72
+ const rightArg = rightStringInfo ? rightStringInfo.value : args[1];
73
+ const event = createPropagationEvent({
74
+ args: [
75
+ {
76
+ tracked: !!leftStringInfo,
77
+ value: leftArg
78
+ },
79
+ {
80
+ tracked: !!rightStringInfo,
81
+ value: rightArg,
82
+ }
83
+ ],
84
+ context: `${inspect(leftArg)} + ${inspect(rightArg)}`,
85
+ moduleName: 'global',
86
+ methodName: 'ContrastMethods.add',
87
+ history,
88
+ object: {
89
+ value: 'String Addition',
90
+ tracked: false
91
+ },
92
+ name: 'ContrastMethods.add',
93
+ result: {
94
+ value: result,
95
+ tracked: true
96
+ },
97
+ source: 'P',
98
+ stacktraceOpts: {
99
+ constructorOpt: add,
100
+ },
101
+ tags: newTags,
102
+ target: 'R',
103
+ });
103
104
 
105
+ if (event) {
104
106
  const { extern } = tracker.track(result, event);
105
-
106
- if (extern) {
107
- data.result = extern;
108
- }
107
+ if (extern) return extern;
109
108
  }
110
109
  }
111
- });
110
+
111
+ return result;
112
+ };
113
+ global.ContrastMethods.add[origSym] = add;
112
114
  },
113
115
  uninstall() {
114
- global.ContrastMethods.add = patcher.unwrap(global.ContrastMethods.add);
116
+ const orig = global.ContrastMethods.add[origSym];
117
+ if (orig) global.ContrastMethods.add = orig;
115
118
  },
116
119
  };
117
120
  };
@@ -16,14 +16,15 @@
16
16
  'use strict';
17
17
 
18
18
  const { isString } = require('@contrast/common');
19
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
19
20
  const { patchType } = require('../../common');
20
21
 
21
22
  module.exports = function (core) {
22
23
  const {
23
24
  logger,
24
- scopes: { instrumentation, sources },
25
25
  patcher,
26
26
  assess: {
27
+ getSourceContext,
27
28
  dataflow: { tracker }
28
29
  }
29
30
  } = core;
@@ -38,13 +39,11 @@ module.exports = function (core) {
38
39
  post(data) {
39
40
  const { args: [value], result } = data;
40
41
  if (
42
+ !tracker.getData(value) ||
41
43
  isNaN(result) ||
42
44
  !value ||
43
45
  !isString(value) ||
44
- !sources.getStore()?.assess ||
45
- instrumentation.isLocked() ||
46
- // why not just do this first? won't need check for NaN, !value, !isString, etc.
47
- !tracker.getData(value)
46
+ !getSourceContext(PROPAGATOR)
48
47
  ) return;
49
48
 
50
49
  tracker.untrack(value);
@@ -16,6 +16,7 @@
16
16
  'use strict';
17
17
 
18
18
  const { DataflowTag } = require('@contrast/common');
19
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
19
20
  const { patchType } = require('../../common');
20
21
 
21
22
  function metadataUpdate(strInfo, event) {
@@ -29,9 +30,9 @@ function metadataUpdate(strInfo, event) {
29
30
 
30
31
  module.exports = function(core) {
31
32
  const {
32
- scopes: { sources, instrumentation },
33
33
  patcher,
34
34
  assess: {
35
+ getSourceContext,
35
36
  eventFactory: { createPropagationEvent },
36
37
  dataflow: { tracker },
37
38
  }
@@ -44,7 +45,7 @@ module.exports = function(core) {
44
45
  name,
45
46
  patchType,
46
47
  post(data) {
47
- if (!data.result || !sources.getStore() || instrumentation.isLocked()) return;
48
+ if (!data.result || !getSourceContext(PROPAGATOR)) return;
48
49
 
49
50
  const arg = data.args[0];
50
51
  let argInfo = tracker.getData(arg);
@@ -15,16 +15,17 @@
15
15
 
16
16
  'use strict';
17
17
 
18
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
18
19
  const { patchType } = require('../../common');
19
20
 
20
21
  module.exports = function(core) {
21
22
  const {
22
23
  assess: {
24
+ getSourceContext,
23
25
  eventFactory: { createPropagationEvent },
24
26
  dataflow: { tracker },
25
27
  },
26
28
  patcher,
27
- scopes: { sources, instrumentation },
28
29
  } = core;
29
30
 
30
31
  const tag = {
@@ -33,11 +34,7 @@ module.exports = function(core) {
33
34
  name: 'ContrastMethods.tag',
34
35
  patchType,
35
36
  post(data) {
36
- if (
37
- !data.result ||
38
- !sources.getStore()?.assess ||
39
- instrumentation.isLocked()
40
- ) {
37
+ if (!data.result || !getSourceContext(PROPAGATOR)) {
41
38
  return;
42
39
  }
43
40
 
@@ -15,17 +15,16 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const {
19
- createAppendTags
20
- } = require('../../../tag-utils');
21
18
  const { join, inspect } = require('@contrast/common');
19
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
20
+ const { createAppendTags } = require('../../../tag-utils');
22
21
  const { patchType } = require('../../common');
23
22
 
24
23
  module.exports = function(core) {
25
24
  const {
26
- scopes: { sources, instrumentation },
27
25
  patcher,
28
26
  assess: {
27
+ getSourceContext,
29
28
  eventFactory: { createPropagationEvent },
30
29
  dataflow: { tracker }
31
30
  }
@@ -40,7 +39,7 @@ module.exports = function(core) {
40
39
  patchType,
41
40
  post(data) {
42
41
  const { args, obj, result, hooked, orig } = data;
43
- if (!result || !sources.getStore()?.assess || instrumentation.isLocked()) return;
42
+ if (!result || !getSourceContext(PROPAGATOR)) return;
44
43
 
45
44
  const rInfo = tracker.getData(result);
46
45
  if (rInfo) {
@@ -15,10 +15,9 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const {
19
- createAppendTags
20
- } = require('../../../tag-utils');
21
18
  const { inspect } = require('@contrast/common');
19
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
20
+ const { createAppendTags } = require('../../../tag-utils');
22
21
  const { patchType } = require('../../common');
23
22
  const htmlTagsLengths = {
24
23
  anchor: 11,
@@ -33,9 +32,9 @@ const htmlTagsLengths = {
33
32
 
34
33
  module.exports = function(core) {
35
34
  const {
36
- scopes: { sources, instrumentation },
37
35
  patcher,
38
36
  assess: {
37
+ getSourceContext,
39
38
  eventFactory: { createPropagationEvent },
40
39
  dataflow: { tracker }
41
40
  }
@@ -65,7 +64,7 @@ module.exports = function(core) {
65
64
  patchType,
66
65
  post(data) {
67
66
  const { args, obj, result, hooked, orig } = data;
68
- if (!result || !sources.getStore()?.assess || instrumentation.isLocked()) return;
67
+ if (!result || !getSourceContext(PROPAGATOR)) return;
69
68
 
70
69
  const objInfo = tracker.getData(obj);
71
70
  const history = objInfo ? new Set([objInfo]) : new Set();
@@ -122,7 +121,7 @@ module.exports = function(core) {
122
121
  patchType,
123
122
  post(data) {
124
123
  const { obj, result, hooked, orig } = data;
125
- if (!result || !sources.getStore()?.assess || instrumentation.isLocked()) return;
124
+ if (!result || !getSourceContext(PROPAGATOR)) return;
126
125
 
127
126
  const objInfo = tracker.getData(obj);
128
127
 
@@ -15,14 +15,15 @@
15
15
 
16
16
  'use strict';
17
17
  const { inspect } = require('@contrast/common');
18
- const { patchType } = require('../../common');
18
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
19
19
  const { createSubsetTags } = require('../../../tag-utils');
20
+ const { patchType } = require('../../common');
20
21
 
21
22
  module.exports = function(core) {
22
23
  const {
23
- scopes: { sources, instrumentation },
24
24
  patcher,
25
25
  assess: {
26
+ getSourceContext,
26
27
  eventFactory: { createPropagationEvent },
27
28
  dataflow: {
28
29
  tracker,
@@ -75,7 +76,7 @@ module.exports = function(core) {
75
76
  });
76
77
  }
77
78
 
78
- return (stringInstrumentation.matchAll = {
79
+ return stringInstrumentation.matchAll = {
79
80
  install() {
80
81
  patcher.patch(String.prototype, 'matchAll', {
81
82
  name,
@@ -87,8 +88,7 @@ module.exports = function(core) {
87
88
  !obj ||
88
89
  !args[0] ||
89
90
  typeof obj !== 'string' ||
90
- !sources.getStore()?.assess ||
91
- instrumentation.isLocked()
91
+ !getSourceContext(PROPAGATOR)
92
92
  )
93
93
  return origFn();
94
94
 
@@ -233,5 +233,5 @@ module.exports = function(core) {
233
233
  uninstall() {
234
234
  String.prototype.matchAll = patcher.unwrap(String.prototype.matchAll);
235
235
  },
236
- });
236
+ };
237
237
  };
@@ -18,20 +18,22 @@
18
18
  const {
19
19
  DataflowTag: { UNTRUSTED },
20
20
  match: origMatch,
21
+ inspect,
22
+ join,
21
23
  substring
22
24
  } = require('@contrast/common');
23
- const { inspect, join } = require('@contrast/common');
24
- const { patchType } = require('../../common');
25
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
25
26
  const { createSubsetTags, createAppendTags } = require('../../../tag-utils');
27
+ const { patchType } = require('../../common');
26
28
 
27
29
  module.exports = function(core) {
28
30
  const {
29
31
  patcher,
30
32
  assess: {
33
+ getSourceContext,
31
34
  eventFactory: { createPropagationEvent },
32
35
  dataflow: { tracker }
33
36
  },
34
- scopes: { sources, instrumentation }
35
37
  } = core;
36
38
 
37
39
  function parseArgs(args) {
@@ -141,7 +143,7 @@ module.exports = function(core) {
141
143
  name,
142
144
  patchType,
143
145
  pre(data) {
144
- if (!sources.getStore()?.assess || instrumentation.isLocked()) return;
146
+ if (!getSourceContext(PROPAGATOR)) return;
145
147
 
146
148
  // setup state
147
149
  data._objInfo = tracker.getData(data.obj);
@@ -155,8 +157,6 @@ module.exports = function(core) {
155
157
  },
156
158
  post(data) {
157
159
  if (
158
- !sources.getStore()?.assess ||
159
- instrumentation.isLocked() ||
160
160
  !data.result ||
161
161
  // todo: can we reuse this optimization in other propagators? e.g those performing substring-like operations
162
162
  !data._accumTags?.[UNTRUSTED] ||
@@ -13,15 +13,16 @@
13
13
  * way not consistent with the End User License Agreement.
14
14
  */
15
15
  'use strict';
16
- const { patchType } = require('../../common');
17
16
  const { inspect, join } = require('@contrast/common');
17
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
18
18
  const { createSubsetTags } = require('../../../tag-utils');
19
+ const { patchType } = require('../../common');
19
20
 
20
21
  module.exports = function(core) {
21
22
  const {
22
- scopes: { sources, instrumentation },
23
23
  patcher,
24
24
  assess: {
25
+ getSourceContext,
25
26
  eventFactory: { createPropagationEvent },
26
27
  dataflow: { tracker }
27
28
  }
@@ -55,7 +56,7 @@ module.exports = function(core) {
55
56
  patchType,
56
57
  post(data) {
57
58
  const { name, args: origArgs, obj, result, hooked, orig } = data;
58
- if (!result || !sources.getStore() || instrumentation.isLocked()) return;
59
+ if (!result || !getSourceContext(PROPAGATOR)) return;
59
60
 
60
61
  const objInfo = tracker.getData(obj);
61
62
  if (!objInfo) return;
@@ -15,16 +15,16 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { patchType } = require('../../common');
19
18
  const { join, inspect } = require('@contrast/common');
19
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
20
20
  const { createSubsetTags } = require('../../../tag-utils');
21
-
21
+ const { patchType } = require('../../common');
22
22
 
23
23
  module.exports = function(core) {
24
24
  const {
25
- scopes: { sources, instrumentation },
26
25
  patcher,
27
26
  assess: {
27
+ getSourceContext,
28
28
  eventFactory,
29
29
  dataflow: { tracker }
30
30
  }
@@ -44,10 +44,9 @@ module.exports = function(core) {
44
44
  !result ||
45
45
  origArgs.length === 0 ||
46
46
  result.length === 0 ||
47
- !sources.getStore() ||
48
47
  typeof obj !== 'string' ||
49
- instrumentation.isLocked() ||
50
- (origArgs.length === 1 && origArgs[0] == null)
48
+ (origArgs.length === 1 && origArgs[0] == null) ||
49
+ !getSourceContext(PROPAGATOR)
51
50
  ) return;
52
51
 
53
52
  const objInfo = tracker.getData(obj);
@@ -15,15 +15,16 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { createSubsetTags } = require('../../../tag-utils');
19
18
  const { join, inspect } = require('@contrast/common');
19
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
20
+ const { createSubsetTags } = require('../../../tag-utils');
20
21
  const { patchType } = require('../../common');
21
22
 
22
23
  module.exports = function(core) {
23
24
  const {
24
- scopes: { sources, instrumentation },
25
25
  patcher,
26
26
  assess: {
27
+ getSourceContext,
27
28
  eventFactory: { createPropagationEvent },
28
29
  dataflow: { tracker }
29
30
  }
@@ -63,7 +64,7 @@ module.exports = function(core) {
63
64
  patchType,
64
65
  post(data) {
65
66
  const { obj, args: origArgs, result, name, hooked, orig } = data;
66
- if (!result || !sources.getStore()?.assess || instrumentation.isLocked()) return;
67
+ if (!result || !getSourceContext(PROPAGATOR)) return;
67
68
 
68
69
  const objInfo = tracker.getData(obj);
69
70
  if (!objInfo) return;
@@ -125,4 +126,3 @@ module.exports = function(core) {
125
126
  },
126
127
  };
127
128
  };
128
-
@@ -15,16 +15,15 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const {
19
- createSubsetTags,
20
- } = require('../../../tag-utils');
18
+ const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
19
+ const { createSubsetTags } = require('../../../tag-utils');
21
20
  const { patchType } = require('../../common');
22
21
 
23
22
  module.exports = function(core) {
24
23
  const {
25
- scopes: { sources, instrumentation },
26
24
  patcher,
27
25
  assess: {
26
+ getSourceContext,
28
27
  eventFactory: { createPropagationEvent },
29
28
  dataflow: { tracker }
30
29
  }
@@ -34,7 +33,7 @@ module.exports = function(core) {
34
33
  return function(data) {
35
34
  const { obj, result, hooked, orig } = data;
36
35
 
37
- if (!result?.length || !sources.getStore()?.assess || instrumentation.isLocked()) {
36
+ if (!result?.length || !getSourceContext(PROPAGATOR)) {
38
37
  return;
39
38
  }
40
39
  const rInfo = tracker.getData(result);
@@ -236,6 +236,49 @@ module.exports = function (core) {
236
236
  return { vulnInfo, reportSafe };
237
237
  };
238
238
 
239
+ /**
240
+ * Truncate `arg` values for reporting and get the adjusted `tags`.
241
+ * If an argument isn't directly relevant to the vulnerability, we report it by its type.
242
+ * If it is relevant, we truncate the value if it is an object or array, but not if it is a string.
243
+ * Ex1: Array truncations will look like `[...'<key>':'<value>'...]`
244
+ * Ex2: Object truncations will look like `{...'<key>':'<value>'...}`
245
+ * In the above examples, `key` will be the last value in the path array, and `value` should be the
246
+ * containing the injection. So whether the argument is a string or an object, the actual string value
247
+ * leading to the injection will not be truncated.
248
+ */
249
+ function getAdjustedFields(origArgs, vulnInfo, vulnArgIdx) {
250
+ const { path, strInfo } = vulnInfo;
251
+ const coercedArgs = [];
252
+ let prefix = '';
253
+ let suffix = '';
254
+
255
+ if (path) {
256
+ let openChar, closeChar;
257
+ if (Array.isArray(origArgs[vulnArgIdx])) {
258
+ openChar = '[';
259
+ closeChar = ']';
260
+ } else {
261
+ openChar = '{';
262
+ closeChar = '}';
263
+ }
264
+ prefix = `${openChar}...'${path[path.length - 1]}':'`;
265
+ suffix = `'...${closeChar}`;
266
+ }
267
+
268
+ for (let i = 0; i < origArgs.length; i++) {
269
+ if (i === vulnArgIdx) {
270
+ coercedArgs.push({ value: `${prefix}${strInfo.value}${suffix}`, tracked: true });
271
+ } else {
272
+ coercedArgs.push({ value: origArgs[i]?.constructor?.name ?? typeof origArgs[i], tracked: false });
273
+ }
274
+ }
275
+
276
+ return {
277
+ args: coercedArgs,
278
+ tags: prefix ? utils.createAppendTags(null, strInfo.tags, prefix.length) : strInfo.tags,
279
+ };
280
+ }
281
+
239
282
  function createAroundHook(entity, name, method, getInfoMethod, vulnerableArgIdxs) {
240
283
  const argsIdxsToCheck = vulnerableArgIdxs || [0];
241
284
  return function (next, data) {
@@ -290,19 +333,13 @@ module.exports = function (core) {
290
333
  : next();
291
334
  }
292
335
 
293
- const { path, strInfo } = vulnInfo;
294
336
  const objName = getObjectName(obj, entity);
295
- const args = origArgs.map((arg, idx) => ({
296
- value: isString(arg) ? arg : inspect(arg, { depth: 4 }),
297
- tracked: idx === vulnArgIdx,
298
- }));
299
-
300
- const tags = path ? utils.createAdjustedQueryTags(path, strInfo.tags, strInfo.value, args[vulnArgIdx].value) : strInfo?.tags;
337
+ const { args, tags } = getAdjustedFields(origArgs, vulnInfo, vulnArgIdx);
301
338
  const resultVal = args[args.length - 1].value.startsWith('[Function') ? '' : 'Promise';
302
339
  const sinkEvent = createSinkEvent({
303
340
  args,
304
341
  context: `${objName}.${method}(${args.map((a, idx) => isString(origArgs[idx]) ? `'${a.value}'` : a.value)})`,
305
- history: [strInfo],
342
+ history: [vulnInfo.strInfo],
306
343
  object: {
307
344
  tracked: false,
308
345
  value: `mongodb.${entity}`,
@@ -16,6 +16,7 @@
16
16
  'use strict';
17
17
 
18
18
  const { InputType } = require('@contrast/common');
19
+ const { InstrumentationType: { SOURCE } } = require('../../../constants');
19
20
  const { patchType } = require('../common');
20
21
 
21
22
  const METHODS = ['json', 'raw', 'text', 'urlencoded'];
@@ -27,12 +28,18 @@ const INPUT_TYPES = {
27
28
  };
28
29
 
29
30
  module.exports = function init(core) {
30
- const { assess, depHooks, logger, patcher, scopes } = core;
31
+ const {
32
+ scopes,
33
+ assess: { dataflow, getSourceContext },
34
+ depHooks,
35
+ logger,
36
+ patcher,
37
+ } = core;
31
38
 
32
39
  const preHook = (data) => {
33
40
  const [req, , next] = data.args;
34
41
  data.args[2] = scopes.wrap(function contrastNext(...args) {
35
- const sourceContext = scopes.sources.getStore()?.assess;
42
+ const sourceContext = getSourceContext(SOURCE);
36
43
 
37
44
  if (!sourceContext) {
38
45
  logger.error({ funcKey: data.funcKey }, 'unable to handle source. Missing `sourceContext`');
@@ -68,7 +75,7 @@ module.exports = function init(core) {
68
75
  }
69
76
 
70
77
  try {
71
- assess.dataflow.sources.handle({
78
+ dataflow.sources.handle({
72
79
  context,
73
80
  data: _data,
74
81
  name: data.name,
@@ -89,7 +96,7 @@ module.exports = function init(core) {
89
96
  });
90
97
  };
91
98
 
92
- assess.dataflow.sources.bodyParser1Instrumentation = {
99
+ core.assess.dataflow.sources.bodyParser1Instrumentation = {
93
100
  install() {
94
101
  depHooks.resolve(
95
102
  { name: 'body-parser', version: '>=1.0.0' },
@@ -127,5 +134,5 @@ module.exports = function init(core) {
127
134
  }
128
135
  };
129
136
 
130
- return assess.dataflow.sources.bodyParser1Instrumentation;
137
+ return core.assess.dataflow.sources.bodyParser1Instrumentation;
131
138
  };
@@ -16,10 +16,11 @@
16
16
  'use strict';
17
17
 
18
18
  const { InputType } = require('@contrast/common');
19
+ const { InstrumentationType: { SOURCE } } = require('../../../constants');
19
20
  const { patchType } = require('../common');
20
21
 
21
22
  module.exports = function init(core) {
22
- const { assess, depHooks, logger, patcher, scopes } = core;
23
+ const { assess, depHooks, logger, patcher } = core;
23
24
 
24
25
  assess.dataflow.sources.cookieParser1Instrumentation = {
25
26
  install() {
@@ -39,11 +40,11 @@ module.exports = function init(core) {
39
40
  const { funcKey } = data;
40
41
  const [req, , next] = data.args;
41
42
  data.args[2] = function contrastNext(...args) {
42
- const sourceContext = scopes.sources.getStore()?.assess;
43
+ const sourceContext = assess.getSourceContext(SOURCE);
43
44
 
44
45
  if (!sourceContext) {
45
46
  logger.error({ funcKey }, 'unable to handle source. Missing `sourceContext`');
46
- return;
47
+ return next(...args);
47
48
  }
48
49
 
49
50
  if (sourceContext.parsedCookies) {
@@ -102,6 +102,21 @@ module.exports = function (core) {
102
102
  sourceContext: store.assess
103
103
  };
104
104
 
105
+ // track the headers and the url.
106
+ //
107
+ // note that req.headers and req.headersDistinct are now (as of v15.1.0)
108
+ // lazily computed using an accessor property.
109
+ //
110
+ // there is no need to track headersDistinct because they are not
111
+ // referenced prior to this point. and, when they are referenced, node
112
+ // populates them with references to the (what will be after code below)
113
+ // already-tracked values in rawHeaders. But headers have already been
114
+ // referenced by node before the 'request' event is emitted by the server,
115
+ // so headers need to be tracked independently of rawHeaders. The way
116
+ // node handles the headers is convoluted; it's easier/safer to track the
117
+ // headers as they are. An attacker could use knowledge of node's handling
118
+ // to craft their attack.
119
+ //
105
120
  [
106
121
  {
107
122
  context: 'req.headers',
@@ -117,18 +132,57 @@ module.exports = function (core) {
117
132
  ...sourceInfo,
118
133
  }
119
134
  ].forEach((sourceData) => {
120
- const { inputType } = sourceData;
121
135
  try {
122
136
  dataflow.sources.handle(sourceData);
123
137
  } catch (err) {
138
+ const { inputType } = sourceData;
124
139
  logger.error({ err, inputType, sourceName }, 'unable to handle http source');
125
140
  }
126
141
  });
127
142
 
128
- for (let i = 0; i < req.rawHeaders.length; i += 2) {
129
- const header = toLowerCase(req.rawHeaders[i]);
130
- req.rawHeaders[i + 1] = req.headers[header];
143
+
144
+ //
145
+ // now track the rawHeaders. headers are complicated because they appear
146
+ // three times: headers, headersDistinct, and rawHeaders and we want to
147
+ // create only one event per header value. that turns out not to be as
148
+ // easy/possible as it sounds, due to the way node handles req.headers.
149
+ //
150
+ // see node's lib/_http_incoming.js for details. interesting optimizations
151
+ // and quirky handling per the RFC. some duplicate headers are joined by
152
+ // default, some are not.
153
+ //
154
+ // but we have to track rawHeaders. they are copied to a separate array
155
+ // because the dataflow.sources.handle() doesn't know about an array where
156
+ // only odd indexes are to be tracked.
157
+ //
158
+ // even though we could track the rawHeaders' keys, we don't because they
159
+ // are not used by any application that i'm aware of. it's easy enough to
160
+ // add here if we find there is an edge case where the application bypasses
161
+ // headers and headersDistinct and uses rawHeaders directly.
162
+ //
163
+ const headerValues = [];
164
+ for (let i = 1; i < req.rawHeaders.length; i += 2) {
165
+ headerValues.push(req.rawHeaders[i]);
166
+ }
167
+
168
+ try {
169
+ dataflow.sources.handle({
170
+ context: 'req.headers',
171
+ inputType: InputType.HEADER,
172
+ data: headerValues,
173
+ ...sourceInfo,
174
+ });
175
+ } catch (err) {
176
+ logger.error({ err, inputType: InputType.HEADER, sourceName }, 'unable to handle http source');
131
177
  }
178
+
179
+ //
180
+ // now that the raw headers are tracked, put each tracked value back
181
+ //
182
+ for (let i = 0; i < headerValues.length; i++) {
183
+ req.rawHeaders[(i << 1) + 1] = headerValues[i];
184
+ }
185
+
132
186
  } catch (err) {
133
187
  logger.error({ err, funcKey: data.funcKey }, 'Error during Assess request handling');
134
188
  }
@@ -15,17 +15,19 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { InputType } = require('@contrast/common');
18
+ const { InputType: { QUERYSTRING: inputType } } = require('@contrast/common');
19
19
  const { patchType } = require('../common');
20
-
21
- const inputType = InputType.QUERYSTRING;
20
+ const { InstrumentationType: { SOURCE } } = require('../../../constants');
22
21
 
23
22
  module.exports = (core) => {
24
23
  const {
25
24
  depHooks,
26
25
  patcher,
27
26
  logger,
28
- assess: { dataflow: { sources } },
27
+ assess: {
28
+ getSourceContext,
29
+ dataflow: { sources }
30
+ },
29
31
  } = core;
30
32
 
31
33
  // Patch `qs`
@@ -36,7 +38,7 @@ module.exports = (core) => {
36
38
  name,
37
39
  patchType,
38
40
  post({ args, hooked, orig, result, funcKey }) {
39
- const sourceContext = core.scopes.sources.getStore()?.assess;
41
+ const sourceContext = getSourceContext(SOURCE);
40
42
 
41
43
  if (!sourceContext) {
42
44
  logger.error({ inputType, funcKey }, 'unable to handle source. Missing `sourceContext`');
@@ -17,9 +17,15 @@
17
17
 
18
18
  const { InputType } = require('@contrast/common');
19
19
  const { patchType } = require('../common');
20
+ const { InstrumentationType: { SOURCE } } = require('../../../constants');
20
21
 
21
22
  module.exports = (core) => {
22
- const { depHooks, patcher, logger } = core;
23
+ const {
24
+ assess: { getSourceContext },
25
+ depHooks,
26
+ patcher,
27
+ logger,
28
+ } = core;
23
29
 
24
30
  core.assess.dataflow.sources.querystringInstrumentation = {
25
31
  install() {
@@ -29,7 +35,7 @@ module.exports = (core) => {
29
35
  name,
30
36
  patchType,
31
37
  post({ args, hooked, orig, result, funcKey }) {
32
- const sourceContext = core.scopes.sources.getStore()?.assess;
38
+ const sourceContext = getSourceContext(SOURCE);
33
39
  const inputType = InputType.QUERYSTRING;
34
40
 
35
41
  if (!sourceContext) return;
@@ -14,6 +14,8 @@
14
14
  */
15
15
  'use strict';
16
16
 
17
+ const { split } = require('@contrast/common');
18
+
17
19
  //
18
20
  // This module implements tag range manipulation functions. There are generally
19
21
  // two types of functions:
@@ -461,8 +463,7 @@ function createAdjustedQueryTags(path, tags, value, argString) {
461
463
  break;
462
464
  }
463
465
  }
464
-
465
- return idx >= 0 ? createAppendTags([], tags, idx) : [...tags];
466
+ return idx >= 0 ? createAppendTags([], tags, idx) : { ...tags };
466
467
  }
467
468
 
468
469
  /**
@@ -484,8 +485,8 @@ function createAdjustedQueryTags(path, tags, value, argString) {
484
485
  * - "?test=str","%3Ftest%3Dstr",{"UNTRUSTED":[0,8]} => {"UNTRUSTED":[3,6,10,12]}
485
486
  */
486
487
  function createEscapeTagRanges(input, result, tags) {
487
- const inputArr = input.split('');
488
- const escapedArr = result.split('');
488
+ const inputArr = split(input, '');
489
+ const escapedArr = split(result, '');
489
490
  const overlap = inputArr.filter((x) => {
490
491
  if (escapedArr.includes(x)) {
491
492
  return x;
@@ -39,7 +39,7 @@ module.exports = function(core) {
39
39
  const ctx = sources.getStore()?.assess;
40
40
 
41
41
  // policy will not exist if assess is altogether disabled for the active request e.g. url exclusion
42
- if (!ctx || !ctx?.policy || instrumentation.isLocked()) return null;
42
+ if (!ctx?.policy || instrumentation.isLocked()) return null;
43
43
 
44
44
  switch (type) {
45
45
  case InstrumentationType.PROPAGATOR: {
@@ -50,6 +50,7 @@ module.exports = function(core) {
50
50
  if (ctx.sourceEventsCount > config.assess.max_context_source_events) return null;
51
51
  break;
52
52
  }
53
+
53
54
  case InstrumentationType.RULE: {
54
55
  const [ruleId] = rest;
55
56
  if (!ruleId) break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/assess",
3
- "version": "1.27.1",
3
+ "version": "1.28.0",
4
4
  "description": "Contrast service providing framework-agnostic Assess support",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -17,7 +17,7 @@
17
17
  "test": "../scripts/test.sh"
18
18
  },
19
19
  "dependencies": {
20
- "@contrast/common": "1.20.1",
20
+ "@contrast/common": "1.21.0",
21
21
  "@contrast/distringuish": "^4.4.0",
22
22
  "@contrast/scopes": "1.4.1"
23
23
  }