@contrast/assess 1.7.0 → 1.9.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 (68) hide show
  1. package/lib/dataflow/event-factory.js +17 -13
  2. package/lib/dataflow/propagation/index.js +5 -0
  3. package/lib/dataflow/propagation/install/JSON/index.js +34 -0
  4. package/lib/dataflow/propagation/install/JSON/parse-fn.js +248 -0
  5. package/lib/dataflow/propagation/install/JSON/parse.js +196 -0
  6. package/lib/dataflow/propagation/install/JSON/stringify.js +292 -0
  7. package/lib/dataflow/propagation/install/array-prototype-join.js +21 -14
  8. package/lib/dataflow/propagation/install/buffer.js +81 -0
  9. package/lib/dataflow/propagation/install/contrast-methods/add.js +2 -0
  10. package/lib/dataflow/propagation/install/contrast-methods/index.js +1 -0
  11. package/lib/dataflow/propagation/install/contrast-methods/number.js +58 -0
  12. package/lib/dataflow/propagation/install/contrast-methods/string.js +53 -6
  13. package/lib/dataflow/propagation/install/contrast-methods/tag.js +3 -1
  14. package/lib/dataflow/propagation/install/decode-uri-component.js +9 -2
  15. package/lib/dataflow/propagation/install/ejs/escape-xml.js +8 -2
  16. package/lib/dataflow/propagation/install/encode-uri-component.js +9 -2
  17. package/lib/dataflow/propagation/install/escape-html.js +13 -5
  18. package/lib/dataflow/propagation/install/escape.js +9 -2
  19. package/lib/dataflow/propagation/install/handlebars-utils-escape-expression.js +11 -4
  20. package/lib/dataflow/propagation/install/isnumeric-0.js +59 -0
  21. package/lib/dataflow/propagation/install/mongoose/index.js +36 -0
  22. package/lib/dataflow/propagation/install/mongoose/schema-string.js +156 -0
  23. package/lib/dataflow/propagation/install/mysql-connection-escape.js +5 -0
  24. package/lib/dataflow/propagation/install/parse-int.js +60 -0
  25. package/lib/dataflow/propagation/install/pug-runtime-escape.js +9 -2
  26. package/lib/dataflow/propagation/install/querystring/parse.js +11 -9
  27. package/lib/dataflow/propagation/install/sequelize.js +6 -3
  28. package/lib/dataflow/propagation/install/sql-template-strings.js +9 -2
  29. package/lib/dataflow/propagation/install/string/concat.js +8 -2
  30. package/lib/dataflow/propagation/install/string/format-methods.js +7 -2
  31. package/lib/dataflow/propagation/install/string/html-methods.js +15 -5
  32. package/lib/dataflow/propagation/install/string/match.js +14 -9
  33. package/lib/dataflow/propagation/install/string/replace.js +22 -14
  34. package/lib/dataflow/propagation/install/string/slice.js +13 -5
  35. package/lib/dataflow/propagation/install/string/split.js +15 -11
  36. package/lib/dataflow/propagation/install/string/substring.js +16 -6
  37. package/lib/dataflow/propagation/install/string/trim.js +3 -0
  38. package/lib/dataflow/propagation/install/unescape.js +9 -2
  39. package/lib/dataflow/propagation/install/url/domain-parsers.js +7 -2
  40. package/lib/dataflow/propagation/install/validator/hooks.js +6 -2
  41. package/lib/dataflow/sinks/install/child-process.js +116 -50
  42. package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +6 -3
  43. package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +7 -4
  44. package/lib/dataflow/sinks/install/fs.js +44 -12
  45. package/lib/dataflow/sinks/install/http.js +5 -2
  46. package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +7 -4
  47. package/lib/dataflow/sinks/install/marsdb.js +3 -0
  48. package/lib/dataflow/sinks/install/mongodb.js +249 -149
  49. package/lib/dataflow/sinks/install/mssql.js +9 -2
  50. package/lib/dataflow/sinks/install/mysql.js +9 -4
  51. package/lib/dataflow/sinks/install/postgres.js +6 -3
  52. package/lib/dataflow/sinks/install/sequelize.js +7 -5
  53. package/lib/dataflow/sinks/install/sqlite3.js +7 -3
  54. package/lib/dataflow/sources/handler.js +141 -26
  55. package/lib/dataflow/sources/index.js +2 -7
  56. package/lib/dataflow/sources/install/body-parser1.js +19 -6
  57. package/lib/dataflow/sources/install/express/index.js +4 -1
  58. package/lib/dataflow/sources/install/express/params.js +81 -0
  59. package/lib/dataflow/sources/install/express/parsedUrl.js +87 -0
  60. package/lib/dataflow/sources/install/http.js +33 -19
  61. package/lib/dataflow/sources/install/querystring.js +75 -0
  62. package/lib/dataflow/tag-utils.js +92 -1
  63. package/lib/dataflow/tracker.js +6 -6
  64. package/lib/index.js +2 -0
  65. package/lib/response-scanning/handlers/utils.js +2 -2
  66. package/lib/session-configuration/index.js +34 -0
  67. package/lib/session-configuration/install/http.js +79 -0
  68. package/package.json +2 -2
@@ -19,7 +19,7 @@ const {
19
19
  InputType,
20
20
  DataflowTag,
21
21
  isString,
22
- traverseValues
22
+ join,
23
23
  } = require('@contrast/common');
24
24
 
25
25
  module.exports = function(core) {
@@ -28,17 +28,17 @@ module.exports = function(core) {
28
28
  dataflow: {
29
29
  sources,
30
30
  tracker,
31
- eventFactory: { createSourceEvent }
31
+ eventFactory
32
32
  }
33
33
  },
34
- createSnapshot,
35
34
  config,
35
+ createSnapshot,
36
36
  logger,
37
37
  } = core;
38
38
 
39
39
  const emptyStack = Object.freeze([]);
40
40
 
41
- sources.createTags = function createTags({ inputType, key, value }) {
41
+ sources.createTags = function createTags({ inputType, fieldName = '', value }) {
42
42
  if (!value?.length) {
43
43
  return null;
44
44
  }
@@ -48,25 +48,30 @@ module.exports = function(core) {
48
48
  [DataflowTag.UNTRUSTED]: [0, stop]
49
49
  };
50
50
 
51
- if (inputType === InputType.HEADER && key.toLowerCase() === 'referer') {
51
+ if (inputType === InputType.HEADER && fieldName.toLowerCase() === 'referer') {
52
52
  tags[DataflowTag.HEADER] = [0, stop];
53
53
  }
54
54
 
55
55
  return tags;
56
56
  };
57
57
 
58
+ sources.createStacktrace = function(stacktraceOpts) {
59
+ return config.assess.stacktraces === 'NONE'
60
+ ? emptyStack
61
+ : createSnapshot(stacktraceOpts)();
62
+ };
63
+
58
64
  sources.handle = function({
59
65
  context,
66
+ keys,
60
67
  name,
61
68
  inputType = InputType.UNKNOWN,
62
69
  stacktraceOpts,
63
70
  data,
64
- sourceContext
71
+ sourceContext,
65
72
  }) {
66
73
  if (!data) return;
67
74
 
68
- const max = config.assess.max_context_source_events;
69
-
70
75
  if (!sourceContext) {
71
76
  core.logger.trace({ inputType, name }, 'skipping assess source handling - no request context');
72
77
  return null;
@@ -76,31 +81,60 @@ module.exports = function(core) {
76
81
  context = inputType;
77
82
  }
78
83
 
84
+ const max = config.assess.max_context_source_events;
85
+ let _data = data;
79
86
  let stack;
80
87
 
81
- traverseValues(data, (path, type, value, obj) => {
88
+ if (keys) {
89
+ _data = {};
90
+ for (const key of keys) {
91
+ _data[key] = data[key];
92
+ }
93
+ }
94
+
95
+ function createEvent({ fieldName, pathName, value }) {
96
+ // create the stacktrace once per call to .handle()
97
+ stack || (stack = sources.createStacktrace(stacktraceOpts));
98
+ return eventFactory.createSourceEvent({
99
+ context: `${context}.${pathName}`,
100
+ name,
101
+ fieldName,
102
+ pathName,
103
+ stack,
104
+ inputType,
105
+ tags: sources.createTags({ inputType, fieldName, value }),
106
+ result: { tracked: true, value },
107
+ });
108
+ }
109
+
110
+ if (Buffer.isBuffer(data) && !tracker.getData(data)) {
111
+ const event = createEvent({ pathName: 'body', value: data, fieldName: '' });
112
+ if (event) {
113
+ tracker.track(data, event);
114
+ }
115
+ return;
116
+ }
117
+
118
+ traverse(_data, (path, fieldName, value, obj) => {
119
+ const pathName = join(path, '.');
120
+
82
121
  if (sourceContext.sourceEventsCount >= max) {
83
122
  core.logger.trace({ inputType, name }, 'exiting assess source handling - %s max events exceeded', max);
84
123
  return true;
85
124
  }
86
125
 
87
126
  if (isString(value) && value.length) {
88
- stack = stack || config.assess.stacktraces === 'NONE'
89
- ? emptyStack
90
- : createSnapshot(stacktraceOpts)();
91
- const key = path[path.length - 1];
92
- const pathName = path.join('.');
93
- const event = createSourceEvent({
94
- context: `${context}.${pathName}`,
95
- name,
96
- fieldName: key,
97
- pathName,
98
- stack,
99
- inputType,
100
- tags: sources.createTags({ inputType, key, value }),
101
- result: { tracked: true, value },
102
- });
127
+ const strInfo = tracker.getData(value);
128
+
129
+ if (strInfo) {
130
+ // TODO: confirm this "layering-on" approach is what we want
131
+ // when the value is tracked the handler wins out and we "re-tracks" the value with new source
132
+ // event metadata. without this step tracker would complain about value already being tracked.
133
+ // alternatively we could treat this more like a propagation event and update existing metadata.
134
+ value = strInfo.value;
135
+ }
103
136
 
137
+ const event = createEvent({ pathName, value, fieldName });
104
138
  if (!event) {
105
139
  core.logger.warn({ inputType, name, pathName, value }, 'unable to create source event');
106
140
  return;
@@ -108,15 +142,96 @@ module.exports = function(core) {
108
142
 
109
143
  const { extern } = tracker.track(value, event);
110
144
  if (extern) {
111
- logger.trace({ extern, key, name, inputType }, 'tracked');
112
- obj[key] = extern;
145
+ logger.trace({ extern, fieldName, name, inputType }, 'tracked');
146
+ obj[fieldName] = extern;
147
+
113
148
  sourceContext.sourceEventsCount++;
114
149
  }
150
+ } else if (Buffer.isBuffer(value) && !tracker.getData(value)) {
151
+ const event = createEvent({ pathName, value, fieldName });
152
+ if (event) {
153
+ tracker.track(value, event);
154
+ } else {
155
+ core.logger.warn({ inputType, name, pathName, value }, 'unable to create source event');
156
+ }
115
157
  }
116
158
  });
117
159
 
160
+ if (keys) {
161
+ for (const key of keys) {
162
+ data[key] = _data[key];
163
+ }
164
+ }
165
+
118
166
  return data;
119
167
  };
120
168
 
121
169
  return sources;
122
170
  };
171
+
172
+ /**
173
+ * A custom traversal function for handling source value tracking efficiently.
174
+ * Implementation was adapted from traversal methods in @contrast/common.
175
+ * @param {any} target object to traverse
176
+ * @param {function} cb function<path, key, value, obj>
177
+ * @param {string[]} path path of node being visted; constructed of nested keys
178
+ * @param {boolean} halt whether to halt traversal; determined by callback
179
+ * @param {Set} visited used to dedupe circular references
180
+ */
181
+ function traverse(target, cb, path = [], visited = new Set()) {
182
+ if (isTraversable(target)) {
183
+ for (const key in target) {
184
+ path.push(key);
185
+
186
+ const value = target[key];
187
+
188
+ if (visited.has(value)) {
189
+ path.pop();
190
+ break;
191
+ }
192
+
193
+ if (isVisitable(value)) {
194
+ const halt = cb(path, key, value, target) === false;
195
+ if (halt) {
196
+ return;
197
+ }
198
+ }
199
+
200
+ if (isTraversable(value)) {
201
+ visited.add(value);
202
+ traverse(value, cb, path, visited);
203
+ }
204
+
205
+ path.pop();
206
+ }
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Visit strings, buffers, basic objects and arrays.
212
+ * @param {any} value the value to check
213
+ * @returns {boolean}
214
+ */
215
+ function isVisitable(value) {
216
+ if (!value) return false;
217
+
218
+ return value.constructor?.name === 'String' ||
219
+ value.constructor?.name === 'Buffer' ||
220
+ (typeof value === 'object' && !value.constructor);
221
+ }
222
+
223
+ /**
224
+ * The criteria for traversal is a strict as possible. We only traverse plain
225
+ * objects and arrays and objects created via Object.create(null).
226
+ * @param {any} value the value to check
227
+ * @returns {boolean}
228
+ */
229
+ function isTraversable(value) {
230
+ if (!value || typeof value !== 'object') return false;
231
+
232
+ return value.constructor?.name === 'Object' ||
233
+ value.constructor?.name === 'Array' ||
234
+ !value.constructor;
235
+ }
236
+
237
+ module.exports.traverse = traverse;
@@ -20,23 +20,18 @@ const { callChildComponentMethodsSync } = require('@contrast/common');
20
20
  module.exports = function (core) {
21
21
  const sources = core.assess.dataflow.sources = {};
22
22
 
23
- // API
24
23
  require('./handler')(core);
25
- // installers
26
- // general
27
- require('./install/http')(core);
28
24
 
29
- // frameworks and frameworks specific libraries
30
25
  require('./install/express')(core);
31
26
  require('./install/fastify')(core);
32
27
  require('./install/koa')(core);
33
-
34
- // libraries
35
28
  require('./install/body-parser1')(core);
36
29
  require('./install/busboy1')(core);
37
30
  require('./install/cookie-parser1')(core);
38
31
  require('./install/formidable1')(core);
32
+ require('./install/http')(core);
39
33
  require('./install/qs6')(core);
34
+ require('./install/querystring')(core);
40
35
 
41
36
  sources.install = function install() {
42
37
  callChildComponentMethodsSync(sources, 'install');
@@ -36,12 +36,12 @@ module.exports = function init(core) {
36
36
 
37
37
  if (!sourceContext) {
38
38
  logger.error({ name }, 'unable to handle source. Missing `sourceContext`');
39
- return;
39
+ return next(...args);
40
40
  }
41
41
 
42
42
  if (sourceContext.parsedBody) {
43
43
  logger.trace({ name }, 'values already tracked');
44
- return;
44
+ return next(...args);
45
45
  }
46
46
 
47
47
  // when using a specific parser, we know the input type.
@@ -55,19 +55,32 @@ module.exports = function init(core) {
55
55
  : InputType.BODY;
56
56
  }
57
57
 
58
+ let keys;
59
+ let _data = req.body;
60
+ let context = 'req.body';
61
+ const isString = typeof _data === 'string';
62
+ const isBuffer = Buffer.isBuffer(_data);
63
+
64
+ if (isString || isBuffer) {
65
+ context = 'req';
66
+ keys = ['body'];
67
+ _data = req;
68
+ }
69
+
58
70
  try {
59
71
  assess.dataflow.sources.handle({
60
- context: 'req.body',
72
+ context,
73
+ data: _data,
61
74
  name,
75
+ keys,
62
76
  inputType,
77
+ sourceContext,
63
78
  stacktraceOpts: {
64
79
  constructorOpt: contrastNext
65
80
  },
66
- data: req.body,
67
- sourceContext,
68
81
  });
69
82
 
70
- sourceContext.parsedBody = !!Object.keys(req.body).length;
83
+ sourceContext.parsedBody = !!Object.keys(_data).length;
71
84
  } catch (err) {
72
85
  logger.error({ name, err }, 'unable to handle source');
73
86
  }
@@ -17,12 +17,15 @@
17
17
 
18
18
  const { callChildComponentMethodsSync } = require('@contrast/common');
19
19
 
20
- module.exports = function init(core) {
20
+ module.exports = function (core) {
21
21
  core.assess.dataflow.sources.expressInstrumentation = {
22
22
  install() {
23
23
  callChildComponentMethodsSync(this, 'install');
24
24
  }
25
25
  };
26
26
 
27
+ require('./params')(core);
28
+ require('./parsedUrl')(core);
29
+
27
30
  return core.assess.dataflow.sources.expressInstrumentation;
28
31
  };
@@ -0,0 +1,81 @@
1
+ /*
2
+ * Copyright: 2022 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const { InputType } = require('@contrast/common');
19
+ const { patchType } = require('../../../propagation/common');
20
+
21
+ module.exports = function init(core) {
22
+ const { depHooks, patcher, logger } = core;
23
+
24
+ core.assess.dataflow.sources.expressInstrumentation.params = {
25
+ install() {
26
+ const name = 'Layer.prototype.match';
27
+ depHooks.resolve(
28
+ { name: 'express', file: 'lib/router/layer.js' },
29
+ (Layer) => {
30
+ patcher.patch(Layer.prototype, 'match', {
31
+ name,
32
+ patchType,
33
+ post(data) {
34
+ const layer = data.obj;
35
+
36
+ // we can exit early if
37
+ // the layer doesn't match the request or
38
+ // the layer doesn't recognize any parameters
39
+ if (!data.result || !layer.keys || layer.keys.length === 0) {
40
+ return;
41
+ }
42
+
43
+ const sourceContext = core.scopes.sources.getStore()?.assess;
44
+
45
+ if (!sourceContext) {
46
+ logger.error({ name }, 'unable to handle source. Missing `sourceContext`');
47
+ return;
48
+ }
49
+
50
+ if (sourceContext.parsedParams) {
51
+ logger.trace({ name }, 'values already tracked');
52
+ return;
53
+ }
54
+
55
+ try {
56
+ core.assess.dataflow.sources.handle({
57
+ context: 'req.params',
58
+ name,
59
+ inputType: InputType.PARAMETER_VALUE,
60
+ stacktraceOpts: {
61
+ constructorOpt: data.hooked,
62
+ prependFrames: [data.orig]
63
+ },
64
+ data: layer.params,
65
+ sourceContext
66
+ });
67
+ sourceContext.parsedParams = true;
68
+ } catch (err) {
69
+ logger.error({ err, name }, 'unable to handle source');
70
+ }
71
+ }
72
+ });
73
+
74
+ return Layer;
75
+ }
76
+ );
77
+ }
78
+ };
79
+
80
+ return core.assess.dataflow.sources.expressInstrumentation.params;
81
+ };
@@ -0,0 +1,87 @@
1
+ /*
2
+ * Copyright: 2022 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const { InputType } = require('@contrast/common');
19
+ const { patchType } = require('../../common');
20
+
21
+ module.exports = function init(core) {
22
+ const {
23
+ assess: {
24
+ dataflow: { sources }
25
+ },
26
+ depHooks,
27
+ patcher,
28
+ scopes
29
+ } = core;
30
+
31
+ core.assess.dataflow.sources.expressInstrumentation.parsedUrl = {
32
+ install() {
33
+ depHooks.resolve(
34
+ { name: 'express', file: 'lib/middleware/init.js' },
35
+ /** @param {import('express/lib/middleware/init')} mw */
36
+ (mw) => {
37
+ const name = 'express.middleware.init';
38
+ patcher.patch(mw, 'init', {
39
+ name,
40
+ patchType,
41
+ post(data) {
42
+ data.result = patcher.patch(data.result, {
43
+ name: 'express.middleware.init.expressInit',
44
+ patchType,
45
+ pre(data) {
46
+ const { args: [req] } = data;
47
+ patcher.patch(data.args, '2', {
48
+ name: 'express.middleware.init.expressInit.next',
49
+ patchType,
50
+ pre(data) {
51
+ const sourceContext = scopes.sources.getStore()?.assess;
52
+ if (!sourceContext) return;
53
+
54
+ const sourceInfo = {
55
+ context: 'req._parsedUrl',
56
+ data: req._parsedUrl,
57
+ name,
58
+ sourceContext,
59
+ stacktraceOpts: {
60
+ constructorOpt: data.hooked
61
+ }
62
+ };
63
+
64
+ sources.handle({
65
+ ...sourceInfo,
66
+ inputType: InputType.URI,
67
+ keys: ['href', 'path', 'pathname'],
68
+ });
69
+
70
+ sources.handle({
71
+ ...sourceInfo,
72
+ inputType: InputType.QUERYSTRING,
73
+ keys: ['query', 'search'],
74
+ });
75
+ }
76
+ });
77
+ }
78
+ });
79
+ }
80
+ });
81
+ }
82
+ );
83
+ }
84
+ };
85
+
86
+ return core.assess.dataflow.sources.expressInstrumentation.parsedUrl;
87
+ };
@@ -19,9 +19,9 @@ const { toLowerCase, InputType } = require('@contrast/common');
19
19
 
20
20
  module.exports = function(core) {
21
21
  const {
22
- scopes: { sources },
22
+ scopes,
23
23
  instrumentation: { instrument },
24
- assess: { dataflow: { sources: dataflowSources } },
24
+ assess: { dataflow },
25
25
  patcher,
26
26
  } = core;
27
27
 
@@ -40,7 +40,7 @@ module.exports = function(core) {
40
40
 
41
41
  try {
42
42
  const [, req, res] = data.args;
43
- const store = sources.getStore();
43
+ const store = scopes.sources.getStore();
44
44
 
45
45
  if (!store) {
46
46
  logger.debug('cannot acquire store for assess request handling');
@@ -80,7 +80,7 @@ module.exports = function(core) {
80
80
  pre(data) {
81
81
  const [name = '', value] = data.args;
82
82
  if (toLowerCase(name) === 'content-type' && value) {
83
- store.assess.responseData.contentType = value;
83
+ scopes.sources.getStore().assess.responseData.contentType = value;
84
84
  }
85
85
  }
86
86
  });
@@ -97,7 +97,6 @@ module.exports = function(core) {
97
97
  }
98
98
 
99
99
  const headers = {};
100
- const sourceInputType = InputType.HEADER;
101
100
  const sourceName = 'ClientRequest';
102
101
 
103
102
  store.assess = {
@@ -107,22 +106,37 @@ module.exports = function(core) {
107
106
  findings: {},
108
107
  };
109
108
 
110
- try {
111
- dataflowSources.handle({
109
+ const sourceInfo = {
110
+ name: sourceName,
111
+ stacktraceOpts: {
112
+ constructorOpt: data.hooked,
113
+ prependFrames: [data.orig]
114
+ },
115
+ sourceContext: store.assess
116
+ };
117
+
118
+ [
119
+ {
112
120
  context: 'req.headers',
121
+ inputType: InputType.HEADER,
113
122
  data: req.headers,
114
- inputType: sourceInputType,
115
- name: sourceName,
116
- stacktraceOpts: {
117
- constructorOpt: data.hooked,
118
- prependFrames: [data.orig]
119
- },
120
- sourceContext: store.assess
121
- });
122
-
123
- } catch (err) {
124
- logger.error({ err, inputType: sourceInputType, name: sourceName }, 'unable to handle http source');
125
- }
123
+ ...sourceInfo,
124
+ },
125
+ {
126
+ context: 'req',
127
+ keys: ['url'],
128
+ inputType: InputType.URI,
129
+ data: req,
130
+ ...sourceInfo,
131
+ }
132
+ ].forEach((sourceData) => {
133
+ const { inputType } = sourceData;
134
+ try {
135
+ dataflow.sources.handle(sourceData);
136
+ } catch (err) {
137
+ logger.error({ err, inputType, name: sourceName }, 'unable to handle http source');
138
+ }
139
+ });
126
140
 
127
141
  for (let i = 0; i < req.rawHeaders.length; i += 2) {
128
142
  const header = toLowerCase(req.rawHeaders[i]);
@@ -0,0 +1,75 @@
1
+ /*
2
+ * Copyright: 2022 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const { InputType } = require('@contrast/common');
19
+ const { patchType } = require('../common');
20
+
21
+ module.exports = (core) => {
22
+ const { depHooks, patcher, logger } = core;
23
+
24
+ core.assess.dataflow.sources.querystringInstrumentation = {
25
+ install() {
26
+ const name = 'querystring.parse';
27
+ depHooks.resolve({ name: 'querystring' },
28
+ (querystring) => patcher.patch(querystring, 'parse', {
29
+ name,
30
+ patchType,
31
+ post({ args, hooked, orig, result }) {
32
+ const sourceContext = core.scopes.sources.getStore()?.assess;
33
+ const inputType = InputType.QUERYSTRING;
34
+
35
+ if (!sourceContext) {
36
+ logger.error({ name }, 'unable to handle source. Missing `sourceContext`');
37
+ return;
38
+ }
39
+
40
+ if (sourceContext.parsedQuery) {
41
+ logger.trace({ name }, 'values already tracked');
42
+ return;
43
+ }
44
+
45
+ // We only run analysis for the `querystring` result when it's used
46
+ // as the framework's query parser
47
+ if (sourceContext.reqData?.queries === args[0]) {
48
+ try {
49
+ core.assess.dataflow.sources.handle({
50
+ context: 'req.query',
51
+ name,
52
+ inputType,
53
+ stacktraceOpts: {
54
+ constructorOpt: hooked,
55
+ prependFrames: [orig]
56
+ },
57
+ data: result,
58
+ sourceContext
59
+ });
60
+
61
+ // we do not set the `parsedQuery` value here so that frameworks
62
+ // may handle queries in their own more specific manner.
63
+ } catch (err) {
64
+ logger.error({ err, name }, 'unable to handle source');
65
+ }
66
+ }
67
+ }
68
+ })
69
+ );
70
+ }
71
+ };
72
+
73
+ return core.assess.dataflow.sources.querystringInstrumentation;
74
+ };
75
+