@contrast/assess 1.5.0 → 1.7.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 (40) hide show
  1. package/lib/dataflow/event-factory.js +10 -5
  2. package/lib/dataflow/propagation/index.js +1 -0
  3. package/lib/dataflow/propagation/install/contrast-methods/string.js +5 -1
  4. package/lib/dataflow/propagation/install/decode-uri-component.js +5 -2
  5. package/lib/dataflow/propagation/install/ejs/escape-xml.js +5 -2
  6. package/lib/dataflow/propagation/install/encode-uri-component.js +5 -2
  7. package/lib/dataflow/propagation/install/escape-html.js +7 -4
  8. package/lib/dataflow/propagation/install/escape.js +5 -2
  9. package/lib/dataflow/propagation/install/handlebars-utils-escape-expression.js +5 -2
  10. package/lib/dataflow/propagation/install/mysql-connection-escape.js +5 -2
  11. package/lib/dataflow/propagation/install/pug-runtime-escape.js +5 -2
  12. package/lib/dataflow/propagation/install/querystring/parse.js +8 -3
  13. package/lib/dataflow/propagation/install/sequelize.js +310 -0
  14. package/lib/dataflow/propagation/install/sql-template-strings.js +5 -4
  15. package/lib/dataflow/propagation/install/string/match.js +2 -2
  16. package/lib/dataflow/propagation/install/string/replace.js +13 -4
  17. package/lib/dataflow/propagation/install/unescape.js +5 -2
  18. package/lib/dataflow/propagation/install/validator/methods.js +60 -51
  19. package/lib/dataflow/sinks/common.js +10 -1
  20. package/lib/dataflow/sinks/index.js +34 -1
  21. package/lib/dataflow/sinks/install/child-process.js +150 -13
  22. package/lib/dataflow/sinks/install/express/index.js +29 -0
  23. package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +134 -0
  24. package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +113 -75
  25. package/lib/dataflow/sinks/install/fs.js +136 -0
  26. package/lib/dataflow/sinks/install/http.js +46 -17
  27. package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +50 -17
  28. package/lib/dataflow/sinks/install/marsdb.js +135 -0
  29. package/lib/dataflow/sinks/install/mongodb.js +322 -0
  30. package/lib/dataflow/sinks/install/mssql.js +19 -10
  31. package/lib/dataflow/sinks/install/mysql.js +138 -0
  32. package/lib/dataflow/sinks/install/postgres.js +37 -23
  33. package/lib/dataflow/sinks/install/sequelize.js +142 -0
  34. package/lib/dataflow/sinks/install/sqlite3.js +20 -10
  35. package/lib/dataflow/sources/handler.js +14 -9
  36. package/lib/dataflow/sources/index.js +4 -1
  37. package/lib/dataflow/sources/install/body-parser1.js +120 -0
  38. package/lib/dataflow/sources/install/cookie-parser1.js +101 -0
  39. package/lib/dataflow/sources/install/express/index.js +28 -0
  40. package/package.json +3 -3
@@ -15,6 +15,9 @@
15
15
 
16
16
  'use strict';
17
17
 
18
+ const {
19
+ DataflowTag: { UNTRUSTED }
20
+ } = require('@contrast/common');
18
21
  const { patchType } = require('../../common');
19
22
  const { createSubsetTags, createAppendTags } = require('../../../tag-utils');
20
23
 
@@ -39,7 +42,7 @@ module.exports = function(core) {
39
42
  const namedGroups = hasNamedGroup ? lastEl : null;
40
43
  return { match, captureGroups, matchIdx, str, namedGroups };
41
44
  }
42
- function replaceSpecialCharacters(replacement, { captureGroups, match, str, namedGroups }) {
45
+ function replaceSpecialCharacters(replacement, { captureGroups, match, str, namedGroups }, replacementType) {
43
46
  const origReplace = patcher.unwrap(String.prototype.replace);
44
47
  let ret = replacement;
45
48
  [
@@ -71,7 +74,7 @@ module.exports = function(core) {
71
74
  }
72
75
  });
73
76
 
74
- const numberedGroupMatches = replacement.match(/\$[1-9][0-9]|\$[1-9]/g);
77
+ const numberedGroupMatches = replacementType !== 'function' && replacement.match(/\$[1-9][0-9]|\$[1-9]/g);
75
78
  if (numberedGroupMatches) {
76
79
  numberedGroupMatches.forEach((numberedGroup) => {
77
80
  const group = Number(numberedGroup.substring(1));
@@ -92,7 +95,7 @@ module.exports = function(core) {
92
95
  data._replacement.call(global, ...replacerArgs) :
93
96
  data._replacement;
94
97
 
95
- replacement = replaceSpecialCharacters(String(replacement), parsedArgs);
98
+ replacement = replaceSpecialCharacters(String(replacement), parsedArgs, data._replacementType);
96
99
 
97
100
  data._replacementInfo = tracker.getData(replacement);
98
101
  if (data._replacement) {
@@ -148,9 +151,15 @@ module.exports = function(core) {
148
151
  !sources.getStore()?.assess ||
149
152
  instrumentation.isLocked() ||
150
153
  !data.result ||
151
- !data._accumTags?.untrusted
154
+ // todo: can we reuse this optimization in other propagators? e.g those performing substring-like operations
155
+ !data._accumTags?.[UNTRUSTED] ||
156
+ !!tracker.getData(data.result)
152
157
  ) return;
153
158
 
159
+ if (data.obj === data.result) {
160
+ return;
161
+ }
162
+
154
163
  const { _replacementInfo, obj, args, result, hooked, orig } = data;
155
164
 
156
165
  const event = createPropagationEvent({
@@ -15,6 +15,9 @@
15
15
 
16
16
  'use strict';
17
17
 
18
+ const {
19
+ DataflowTag: { WEAK_URL_ENCODED }
20
+ } = require('@contrast/common');
18
21
  const {
19
22
  createFullLengthCopyTags
20
23
  } = require('../../tag-utils');
@@ -45,7 +48,7 @@ module.exports = function(core) {
45
48
  const resultInfo = tracker.getData(result);
46
49
  const history = [argInfo];
47
50
  const newTags = createFullLengthCopyTags(argInfo.tags, result.length);
48
- delete newTags['weak-url-encoded'];
51
+ delete newTags[WEAK_URL_ENCODED];
49
52
 
50
53
  if (!Object.keys(newTags).length) return;
51
54
 
@@ -62,7 +65,7 @@ module.exports = function(core) {
62
65
  args: [{ value: argInfo.value, tracked: true }],
63
66
  tags: newTags,
64
67
  history,
65
- removedTags: ['weak-url-encoded'],
68
+ removedTags: [WEAK_URL_ENCODED],
66
69
  stacktraceOpts: {
67
70
  constructorOpt: hooked,
68
71
  prependFrames: [orig]
@@ -15,57 +15,66 @@
15
15
 
16
16
  'use strict';
17
17
 
18
+ const {
19
+ DataflowTag: {
20
+ ALPHANUM_SPACE_HYPHEN,
21
+ CUSTOM_VALIDATED,
22
+ HTML_ENCODED,
23
+ LIMITED_CHARS,
24
+ }
25
+ } = require('@contrast/common');
26
+
18
27
  module.exports = {
19
28
  validators: {
20
- isAfter: 'alphanum-space-hyphen',
21
- isAlpha: 'alphanum-space-hyphen',
22
- isAlphanumeric: 'alphanum-space-hyphen',
23
- isBase32: 'alphanum-space-hyphen',
24
- isBase58: 'alphanum-space-hyphen',
25
- isBase64: 'alphanum-space-hyphen',
26
- isBefore: 'alphanum-space-hyphen',
27
- isBIC: 'alphanum-space-hyphen',
28
- isBoolean: 'limited-chars',
29
- isBtcAddress: 'alphanum-space-hyphen',
30
- isCreditCard: 'limited-chars',
31
- isDate: 'limited-chars',
32
- isDecimal: 'limited-chars',
33
- isEAN: 'limited-chars',
34
- isEthereumAddress: 'alphanum-space-hyphen',
35
- isFloat: 'limited-chars',
36
- isHash: 'alphanum-space-hyphen',
37
- isHexadecimal: 'alphanum-space-hyphen',
38
- isHexColor: 'alphanum-space-hyphen',
39
- isHSL: 'alphanum-space-hyphen',
40
- isIBAN: 'limited-chars',
41
- isIdentityCard: 'alphanum-space-hyphen',
42
- isIMEI: 'limited-chars',
43
- isInt: 'limited-chars',
44
- isIP: 'limited-chars',
45
- isIPRange: 'limited-chars',
46
- isISBN: 'limited-chars',
47
- isISIN: 'limited-chars',
48
- isISO8601: 'alphanum-space-hyphen',
49
- isISO31661Alpha2: 'alphanum-space-hyphen',
50
- isISO31661Alpha3: 'alphanum-space-hyphen',
51
- isISRC: 'alphanum-space-hyphen',
52
- isISSN: 'limited-chars',
53
- isJWT: 'alphanum-space-hyphen',
54
- isLatLong: 'limited-chars',
55
- isLicensePlate: 'alphanum-space-hyphen',
56
- isMACAddress: 'alphanum-space-hyphen',
57
- isMagnetURI: 'alphanum-space-hyphen',
58
- isMD5: 'alphanum-space-hyphen',
59
- isMobilePhone: 'limited-chars',
60
- isNumeric: 'limited-chars',
61
- isOctal: 'alphanum-space-hyphen',
62
- isPassportNumber: 'limited-chars',
63
- isPostalCode: 'limited-chars',
64
- isSemVer: 'limited-chars',
65
- isTaxID: 'limited-chars',
66
- isUUID: 'alphanum-space-hyphen',
67
- isVAT: 'alphanum-space-hyphen',
68
- matches: 'custom-validated'
29
+ isAfter: ALPHANUM_SPACE_HYPHEN,
30
+ isAlpha: ALPHANUM_SPACE_HYPHEN,
31
+ isAlphanumeric: ALPHANUM_SPACE_HYPHEN,
32
+ isBase32: ALPHANUM_SPACE_HYPHEN,
33
+ isBase58: ALPHANUM_SPACE_HYPHEN,
34
+ isBase64: ALPHANUM_SPACE_HYPHEN,
35
+ isBefore: ALPHANUM_SPACE_HYPHEN,
36
+ isBIC: ALPHANUM_SPACE_HYPHEN,
37
+ isBoolean: LIMITED_CHARS,
38
+ isBtcAddress: ALPHANUM_SPACE_HYPHEN,
39
+ isCreditCard: LIMITED_CHARS,
40
+ isDate: LIMITED_CHARS,
41
+ isDecimal: LIMITED_CHARS,
42
+ isEAN: LIMITED_CHARS,
43
+ isEthereumAddress: ALPHANUM_SPACE_HYPHEN,
44
+ isFloat: LIMITED_CHARS,
45
+ isHash: ALPHANUM_SPACE_HYPHEN,
46
+ isHexadecimal: ALPHANUM_SPACE_HYPHEN,
47
+ isHexColor: ALPHANUM_SPACE_HYPHEN,
48
+ isHSL: ALPHANUM_SPACE_HYPHEN,
49
+ isIBAN: LIMITED_CHARS,
50
+ isIdentityCard: ALPHANUM_SPACE_HYPHEN,
51
+ isIMEI: LIMITED_CHARS,
52
+ isInt: LIMITED_CHARS,
53
+ isIP: LIMITED_CHARS,
54
+ isIPRange: LIMITED_CHARS,
55
+ isISBN: LIMITED_CHARS,
56
+ isISIN: LIMITED_CHARS,
57
+ isISO8601: ALPHANUM_SPACE_HYPHEN,
58
+ isISO31661Alpha2: ALPHANUM_SPACE_HYPHEN,
59
+ isISO31661Alpha3: ALPHANUM_SPACE_HYPHEN,
60
+ isISRC: ALPHANUM_SPACE_HYPHEN,
61
+ isISSN: LIMITED_CHARS,
62
+ isJWT: ALPHANUM_SPACE_HYPHEN,
63
+ isLatLong: LIMITED_CHARS,
64
+ isLicensePlate: ALPHANUM_SPACE_HYPHEN,
65
+ isMACAddress: ALPHANUM_SPACE_HYPHEN,
66
+ isMagnetURI: ALPHANUM_SPACE_HYPHEN,
67
+ isMD5: ALPHANUM_SPACE_HYPHEN,
68
+ isMobilePhone: LIMITED_CHARS,
69
+ isNumeric: LIMITED_CHARS,
70
+ isOctal: ALPHANUM_SPACE_HYPHEN,
71
+ isPassportNumber: LIMITED_CHARS,
72
+ isPostalCode: LIMITED_CHARS,
73
+ isSemVer: LIMITED_CHARS,
74
+ isTaxID: LIMITED_CHARS,
75
+ isUUID: ALPHANUM_SPACE_HYPHEN,
76
+ isVAT: ALPHANUM_SPACE_HYPHEN,
77
+ matches: CUSTOM_VALIDATED,
69
78
  },
70
79
  untrackers: [
71
80
  'equals',
@@ -74,9 +83,9 @@ module.exports = {
74
83
  'isRFC3339'
75
84
  ],
76
85
  sanitizers: {
77
- escape: 'html-encoded'
86
+ escape: HTML_ENCODED
78
87
  },
79
88
  custom: {
80
- isEmail: 'limited-chars'
89
+ isEmail: LIMITED_CHARS
81
90
  }
82
91
  };
@@ -16,5 +16,14 @@
16
16
  'use strict';
17
17
 
18
18
  module.exports = {
19
- patchType: 'assess-dataflow-sink'
19
+ patchType: 'assess-dataflow-sink',
20
+
21
+ /**
22
+ * @param {string[]} safeTags array of sink's safe tags
23
+ * @param {object} strInfo tracked string info
24
+ * @returns {string[]} filtered list of safe tags found on tracked string data
25
+ */
26
+ filterSafeTags(safeTags, strInfo) {
27
+ return safeTags.filter((t) => !!strInfo.tags?.[t]);
28
+ },
20
29
  };
@@ -15,19 +15,33 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { callChildComponentMethodsSync, Event } = require('@contrast/common');
18
+ const { AsyncLocalStorage } = require('async_hooks');
19
+ const { callChildComponentMethodsSync, Event, Rule } = require('@contrast/common');
19
20
  const { isVulnerable } = require('../utils/is-vulnerable');
20
21
  const { isSafeContentType } = require('../utils/is-safe-content-type');
21
22
 
22
23
  module.exports = function (core) {
23
24
  const {
25
+ logger,
24
26
  messages,
25
27
  scopes: { sources }
26
28
  } = core;
27
29
 
30
+ const sinkScopes = {
31
+ [Rule.SQL_INJECTION]: new AsyncLocalStorage(),
32
+ [Rule.NOSQL_INJECTION_MONGO]: new AsyncLocalStorage()
33
+ };
28
34
  const sinks = core.assess.dataflow.sinks = {
29
35
  isVulnerable,
30
36
  isSafeContentType,
37
+ reportSafePositive(data) {
38
+ const store = sources.getStore();
39
+ // these events need source correlation
40
+ messages.emit(Event.ASSESS_DATAFLOW_SAFE_POSITIVE, {
41
+ sourceInfo: store?.sourceInfo,
42
+ ...data
43
+ });
44
+ },
31
45
  reportFindings(data) {
32
46
  const store = sources.getStore();
33
47
  // these events need source correlation
@@ -36,15 +50,34 @@ module.exports = function (core) {
36
50
  ...data
37
51
  });
38
52
  },
53
+ runInActiveSink(rule, cb) {
54
+ if (sinkScopes[rule]) {
55
+ return sinkScopes[rule].run({ locked: true }, cb);
56
+ } else {
57
+ logger.error('Sink scope for %s rule does not exist', rule);
58
+ return cb();
59
+ }
60
+ },
61
+ isLocked(rule) {
62
+ if (!sinkScopes[rule]) return false;
63
+
64
+ return sinkScopes[rule].getStore()?.locked;
65
+ }
39
66
  };
40
67
 
41
68
  require('./install/fastify')(core);
42
69
  require('./install/koa')(core);
43
70
  require('./install/child-process')(core);
71
+ require('./install/fs')(core);
44
72
  require('./install/http')(core);
73
+ require('./install/mongodb')(core);
45
74
  require('./install/mssql')(core);
75
+ require('./install/mysql')(core);
46
76
  require('./install/postgres')(core);
77
+ require('./install/sequelize')(core);
47
78
  require('./install/sqlite3')(core);
79
+ require('./install/marsdb')(core);
80
+ require('./install/express')(core);
48
81
 
49
82
  sinks.install = function() {
50
83
  callChildComponentMethodsSync(core.assess.dataflow.sinks, 'install');
@@ -14,8 +14,12 @@
14
14
  */
15
15
 
16
16
  'use strict';
17
+ const {
18
+ DataflowTag: { UNTRUSTED }
19
+ } = require('@contrast/common');
20
+
17
21
  const { patchType } = require('../common');
18
- const { Rule, isString } = require('@contrast/common');
22
+ const { Rule, isString, inspect } = require('@contrast/common');
19
23
 
20
24
  module.exports = function (core) {
21
25
  const {
@@ -31,14 +35,14 @@ module.exports = function (core) {
31
35
  },
32
36
  } = core;
33
37
 
34
- const pre = (name) => (data) => {
35
- const store = sources.getStore()?.assess;
36
- if (!store || !data.args[0] || !isString(data.args[0])) return;
38
+ const safeTags = [];
37
39
 
38
- const strInfo = tracker.getData(data.args[0]);
39
- if (!strInfo || !isVulnerable('untrusted', [], strInfo.tags)) {
40
- return;
41
- }
40
+ function commandCheck(name, command, secondArg, thirdArg, hooked) {
41
+ const strInfo = tracker.getData(command);
42
+ if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) return {
43
+ strInfo: null,
44
+ reported: false
45
+ };
42
46
 
43
47
  const event = createSinkEvent({
44
48
  name,
@@ -52,11 +56,19 @@ module.exports = function (core) {
52
56
  value: strInfo.value,
53
57
  tracked: true,
54
58
  },
55
- ],
59
+ (secondArg && {
60
+ value: inspect(secondArg),
61
+ tracked: false
62
+ }),
63
+ (thirdArg && {
64
+ value: inspect(thirdArg),
65
+ tracked: false
66
+ })
67
+ ].filter(Boolean),
56
68
  tags: strInfo.tags,
57
69
  source: 'P0',
58
70
  stacktraceOpts: {
59
- contructorOpt: data.hooked,
71
+ constructorOpt: hooked,
60
72
  },
61
73
  });
62
74
 
@@ -65,18 +77,143 @@ module.exports = function (core) {
65
77
  ruleId: Rule.CMD_INJECTION,
66
78
  sinkEvent: event,
67
79
  });
80
+
81
+ return {
82
+ strInfo,
83
+ reported: true
84
+ };
68
85
  }
69
- };
86
+
87
+ return {
88
+ strInfo,
89
+ reported: false
90
+ };
91
+ }
92
+
93
+ function argumentsCheck(name, command, commandInfo, args, options, hooked) {
94
+ if (!Array.isArray(args) || !args?.length) return;
95
+
96
+ const trackedArgs = [];
97
+ let vulnerableArgIdx;
98
+
99
+ for (let i = 0; i < args.length; i++) {
100
+ const trackData = tracker.getData(args[i]);
101
+
102
+ trackedArgs.push(trackData);
103
+
104
+ if (!trackData) {
105
+ continue;
106
+ }
107
+
108
+ if (
109
+ !vulnerableArgIdx &&
110
+ vulnerableArgIdx != 0 &&
111
+ isVulnerable(UNTRUSTED, safeTags, trackData.tags)
112
+ ) {
113
+ vulnerableArgIdx = i;
114
+ }
115
+ }
116
+
117
+ if (vulnerableArgIdx != 0 && !vulnerableArgIdx) return;
118
+
119
+ const event = createSinkEvent({
120
+ name,
121
+ history: [trackedArgs[vulnerableArgIdx]],
122
+ object: {
123
+ value: 'child_process',
124
+ tracked: false,
125
+ },
126
+ args: [
127
+ {
128
+ value: commandInfo?.value || command,
129
+ tracked: !!commandInfo,
130
+ },
131
+ {
132
+ value: inspect(args),
133
+ tracked: true
134
+ },
135
+ {
136
+ value: inspect(options),
137
+ tracked: false
138
+ }
139
+ ],
140
+ tags: trackedArgs[vulnerableArgIdx].tags,
141
+ source: 'P1',
142
+ stacktraceOpts: {
143
+ contructorOpt: hooked,
144
+ },
145
+ });
146
+
147
+ if (event) {
148
+ reportFindings({
149
+ ruleId: Rule.CMD_INJECTION,
150
+ sinkEvent: event,
151
+ });
152
+ }
153
+ }
70
154
 
71
155
  core.assess.dataflow.sinks.cmdInjection = {
72
156
  install() {
73
157
  depHooks.resolve({ name: 'child_process' }, cp => {
74
- ['spawn', 'spawnSync', 'exec', 'execSync'].forEach((method) => {
158
+ ['spawn', 'spawnSync'].forEach((method) => {
159
+ const name = `child_process.${method}`;
160
+ patcher.patch(cp, method, {
161
+ name,
162
+ patchType,
163
+ pre(data) {
164
+ const store = sources.getStore()?.assess;
165
+ const [command] = data.args;
166
+
167
+ if (!store || !command || !isString(command)) return;
168
+
169
+ const cpArgs = Array.isArray(data.args[1]) && data.args[1];
170
+ const options = cpArgs ? data.args[2] : data.args[1];
171
+
172
+ const cmdCheck = commandCheck(name, command, cpArgs, options, data.hooked);
173
+
174
+ if (cmdCheck.reported || !options?.shell) return;
175
+
176
+ argumentsCheck(name, command, cmdCheck.strInfo, cpArgs, options, data.hooked);
177
+ }
178
+ });
179
+ });
180
+
181
+ ['exec', 'execSync'].forEach((method) => {
182
+ const name = `child_process.${method}`;
183
+ patcher.patch(cp, method, {
184
+ name,
185
+ patchType,
186
+ pre(data) {
187
+ const store = sources.getStore()?.assess;
188
+ const [command, secondArg, thirdArg] = data.args;
189
+
190
+ if (!store || !command || !isString(command)) return;
191
+
192
+ commandCheck(name, command, secondArg, thirdArg, data.hooked);
193
+ }
194
+ });
195
+ });
196
+
197
+ ['execFile', 'execFileSync'].forEach((method) => {
75
198
  const name = `child_process.${method}`;
76
199
  patcher.patch(cp, method, {
77
200
  name,
78
201
  patchType,
79
- pre: pre(name)
202
+ pre(data) {
203
+ const store = sources.getStore()?.assess;
204
+ const [command] = data.args;
205
+
206
+ if (!store || !command || !isString(command)) return;
207
+
208
+ const cpArgs = Array.isArray(data.args[1]) && data.args[1];
209
+ const options = cpArgs ? data.args[2] : data.args[1];
210
+
211
+ if (!options?.shell) return;
212
+
213
+ const cmdInfo = tracker.getData(command);
214
+
215
+ argumentsCheck(name, command, cmdInfo, cpArgs, options, data.hooked);
216
+ }
80
217
  });
81
218
  });
82
219
  });
@@ -0,0 +1,29 @@
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
+ 'use strict';
16
+
17
+ const { callChildComponentMethodsSync } = require('@contrast/common');
18
+
19
+ module.exports = function(core) {
20
+ const express = core.assess.dataflow.sinks.express = {};
21
+
22
+ require('./unvalidated-redirect')(core);
23
+
24
+ express.install = function() {
25
+ callChildComponentMethodsSync(express, 'install');
26
+ };
27
+
28
+ return express;
29
+ };
@@ -0,0 +1,134 @@
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 util = require('util');
19
+ const {
20
+ DataflowTag: {
21
+ UNTRUSTED,
22
+ CUSTOM_ENCODED,
23
+ CUSTOM_VALIDATED,
24
+ HTML_ENCODED,
25
+ LIMITED_CHARS,
26
+ URL_ENCODED,
27
+ },
28
+ isString
29
+ } = require('@contrast/common');
30
+ const { patchType, filterSafeTags } = require('../../common');
31
+ const { createSubsetTags } = require('../../../tag-utils');
32
+
33
+ const ruleId = 'unvalidated-redirect';
34
+
35
+ module.exports = function (core) {
36
+ const {
37
+ depHooks,
38
+ patcher,
39
+ config,
40
+ scopes: { sources },
41
+ assess: {
42
+ dataflow: {
43
+ tracker,
44
+ sinks: { isVulnerable, reportFindings, reportSafePositive },
45
+ eventFactory: { createSinkEvent },
46
+ },
47
+ },
48
+ } = core;
49
+ const unvalidatedRedirect =
50
+ (core.assess.dataflow.sinks.express.unvalidatedRedirect = {});
51
+
52
+ const inspect = patcher.unwrap(util.inspect);
53
+
54
+ const safeTags = [
55
+ CUSTOM_ENCODED,
56
+ CUSTOM_VALIDATED,
57
+ HTML_ENCODED,
58
+ LIMITED_CHARS,
59
+ URL_ENCODED,
60
+ ];
61
+
62
+ unvalidatedRedirect.install = function () {
63
+ depHooks.resolve({ name: 'express', file: 'lib/response' }, (Response) => {
64
+ const name = 'Express.Response.location';
65
+ patcher.patch(Response, 'location', {
66
+ name: 'Express.Response.location',
67
+ patchType,
68
+ pre: (data) => {
69
+ const assessStore = sources.getStore()?.assess;
70
+ if (!assessStore) return;
71
+
72
+ let [url] = data.args;
73
+ if (url === 'back') {
74
+ url = data.obj.req.get('Referrer');
75
+ }
76
+
77
+ if (!url || !isString(url)) return;
78
+
79
+ const strInfo = tracker.getData(url);
80
+ if (!strInfo) return;
81
+
82
+ let urlPathTags = strInfo.tags;
83
+ if (url.indexOf('?') > -1) {
84
+ urlPathTags = createSubsetTags(strInfo.tags, 0, url.indexOf('?'));
85
+ }
86
+
87
+ if (urlPathTags && isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
88
+ const event = createSinkEvent({
89
+ args: [{
90
+ tracked: true,
91
+ value: strInfo.value,
92
+ }],
93
+ context: `response.location(${inspect(strInfo.value)})`,
94
+ history: [strInfo],
95
+ name: 'Express.Response.location',
96
+ object: {
97
+ tracked: false,
98
+ value: 'Express.Response',
99
+ },
100
+ result: {
101
+ tracked: false,
102
+ value: undefined,
103
+ },
104
+ tags: urlPathTags,
105
+ source: 'P0',
106
+ stacktraceOpts: {
107
+ constructorOpt: data.hooked,
108
+ },
109
+ });
110
+
111
+ if (event) {
112
+ reportFindings({
113
+ ruleId,
114
+ sinkEvent: event,
115
+ });
116
+ }
117
+ } else if (config.assess.safe_positives.enable) {
118
+ reportSafePositive({
119
+ name,
120
+ ruleId,
121
+ safeTags: filterSafeTags(safeTags, strInfo),
122
+ strInfo: {
123
+ tags: strInfo.tags,
124
+ value: strInfo.value,
125
+ }
126
+ });
127
+ }
128
+ },
129
+ });
130
+ });
131
+ };
132
+
133
+ return unvalidatedRedirect;
134
+ };