@contrast/assess 1.6.0 → 1.8.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 (35) hide show
  1. package/lib/dataflow/propagation/index.js +3 -0
  2. package/lib/dataflow/propagation/install/JSON/index.js +33 -0
  3. package/lib/dataflow/propagation/install/JSON/stringify.js +290 -0
  4. package/lib/dataflow/propagation/install/buffer.js +79 -0
  5. package/lib/dataflow/propagation/install/contrast-methods/string.js +5 -1
  6. package/lib/dataflow/propagation/install/encode-uri-component.js +5 -2
  7. package/lib/dataflow/propagation/install/pug-runtime-escape.js +5 -2
  8. package/lib/dataflow/propagation/install/sequelize.js +310 -0
  9. package/lib/dataflow/propagation/install/sql-template-strings.js +5 -4
  10. package/lib/dataflow/propagation/install/string/match.js +2 -2
  11. package/lib/dataflow/propagation/install/string/replace.js +9 -4
  12. package/lib/dataflow/sinks/common.js +10 -1
  13. package/lib/dataflow/sinks/index.js +30 -1
  14. package/lib/dataflow/sinks/install/express/index.js +29 -0
  15. package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +134 -0
  16. package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +96 -69
  17. package/lib/dataflow/sinks/install/http.js +20 -5
  18. package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +33 -9
  19. package/lib/dataflow/sinks/install/mongodb.js +297 -82
  20. package/lib/dataflow/sinks/install/mssql.js +9 -4
  21. package/lib/dataflow/sinks/install/mysql.js +20 -4
  22. package/lib/dataflow/sinks/install/postgres.js +25 -12
  23. package/lib/dataflow/sinks/install/sequelize.js +142 -0
  24. package/lib/dataflow/sinks/install/sqlite3.js +9 -4
  25. package/lib/dataflow/sources/handler.js +144 -26
  26. package/lib/dataflow/sources/index.js +6 -8
  27. package/lib/dataflow/sources/install/body-parser1.js +133 -0
  28. package/lib/dataflow/sources/install/cookie-parser1.js +101 -0
  29. package/lib/dataflow/sources/install/express/index.js +31 -0
  30. package/lib/dataflow/sources/install/express/params.js +81 -0
  31. package/lib/dataflow/sources/install/express/parsedUrl.js +87 -0
  32. package/lib/dataflow/sources/install/http.js +32 -18
  33. package/lib/dataflow/sources/install/querystring.js +75 -0
  34. package/lib/dataflow/tag-utils.js +68 -1
  35. package/package.json +3 -3
@@ -0,0 +1,31 @@
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 { callChildComponentMethodsSync } = require('@contrast/common');
19
+
20
+ module.exports = function (core) {
21
+ core.assess.dataflow.sources.expressInstrumentation = {
22
+ install() {
23
+ callChildComponentMethodsSync(this, 'install');
24
+ }
25
+ };
26
+
27
+ require('./params')(core);
28
+ require('./parsedUrl')(core);
29
+
30
+ return core.assess.dataflow.sources.expressInstrumentation;
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');
@@ -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
+
@@ -68,6 +68,57 @@ function atomicSubset(tags, subsetStart, len) {
68
68
  return ret;
69
69
  }
70
70
 
71
+ function atomicMerge(firstTagRanges, secondTagRanges) {
72
+ const mergedRanges = [];
73
+ let i = 0;
74
+ let j = 0;
75
+
76
+ while (i < firstTagRanges.length && j < secondTagRanges.length) {
77
+ const start1 = firstTagRanges[i];
78
+ const end1 = firstTagRanges[i + 1];
79
+ const start2 = secondTagRanges[j];
80
+ const end2 = secondTagRanges[j + 1];
81
+
82
+ if (end1 < start2) {
83
+ mergedRanges.push(start1, end1);
84
+ i += 2;
85
+ } else if (end2 < start1) {
86
+ mergedRanges.push(start2, end2);
87
+ j += 2;
88
+ } else {
89
+ const mergedStart = Math.min(start1, start2);
90
+ const mergedEnd = Math.max(end1, end2);
91
+ mergedRanges.push(mergedStart, mergedEnd);
92
+ i += 2;
93
+ j += 2;
94
+ }
95
+ }
96
+
97
+ while (i < firstTagRanges.length) {
98
+ mergedRanges.push(firstTagRanges[i], firstTagRanges[i + 1]);
99
+ i += 2;
100
+ }
101
+
102
+ while (j < secondTagRanges.length) {
103
+ mergedRanges.push(secondTagRanges[j], secondTagRanges[j + 1]);
104
+ j += 2;
105
+ }
106
+
107
+ // Merge adjacent ranges
108
+ const finalMergedRanges = [];
109
+ for (let k = 0; k < mergedRanges.length; k += 2) {
110
+ const start = mergedRanges[k];
111
+ let end = mergedRanges[k + 1];
112
+ while (k + 2 < mergedRanges.length && mergedRanges[k + 2] - end === 1) {
113
+ end = mergedRanges[k + 3];
114
+ k += 2;
115
+ }
116
+ finalMergedRanges.push(start, end);
117
+ }
118
+
119
+ return finalMergedRanges;
120
+ }
121
+
71
122
  function createAppendTags(firstTags, secondTags, offset) {
72
123
  const ret = Object.create(null);
73
124
  const firstTagsObject = ensureObject(firstTags);
@@ -115,8 +166,24 @@ function createFullLengthCopyTags(tags, resultLength) {
115
166
  return Object.keys(ret).length ? ret : null;
116
167
  }
117
168
 
169
+ function createMergedTags(firstTags, secondTags) {
170
+ const ret = Object.create(null);
171
+ const firstTagsObject = ensureObject(firstTags);
172
+ const secondTagsObject = ensureObject(secondTags);
173
+ const tagNames = new Set([...Object.keys(firstTagsObject), ...Object.keys(secondTagsObject)]);
174
+
175
+ for (const tagName of tagNames) {
176
+ const newTagRanges = atomicMerge(ensureTagsImmutable(firstTagsObject, tagName), ensureTagsImmutable(secondTagsObject, tagName));
177
+
178
+ newTagRanges.length && (ret[tagName] = newTagRanges);
179
+ }
180
+
181
+ return Object.keys(ret).length ? ret : null;
182
+ }
183
+
118
184
  module.exports = {
119
185
  createSubsetTags,
120
186
  createAppendTags,
121
- createFullLengthCopyTags
187
+ createFullLengthCopyTags,
188
+ createMergedTags
122
189
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/assess",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -14,8 +14,8 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@contrast/distringuish": "^4.1.0",
17
- "@contrast/scopes": "1.3.0",
18
- "@contrast/common": "1.9.0",
17
+ "@contrast/scopes": "1.4.0",
18
+ "@contrast/common": "1.11.0",
19
19
  "parseurl": "^1.3.3"
20
20
  }
21
21
  }