@contrast/assess 1.8.0 → 1.10.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 (75) hide show
  1. package/lib/dataflow/event-factory.js +17 -13
  2. package/lib/dataflow/propagation/index.js +4 -0
  3. package/lib/dataflow/propagation/install/JSON/index.js +1 -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 +5 -3
  7. package/lib/dataflow/propagation/install/array-prototype-join.js +21 -14
  8. package/lib/dataflow/propagation/install/buffer.js +2 -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/path/basename.js +124 -0
  26. package/lib/dataflow/propagation/install/path/common.js +176 -0
  27. package/lib/dataflow/propagation/install/path/index.js +32 -0
  28. package/lib/dataflow/propagation/install/path/join-and-resolve.js +141 -0
  29. package/lib/dataflow/propagation/install/path/normalize.js +123 -0
  30. package/lib/dataflow/propagation/install/pug-runtime-escape.js +9 -2
  31. package/lib/dataflow/propagation/install/querystring/parse.js +12 -10
  32. package/lib/dataflow/propagation/install/sequelize.js +6 -3
  33. package/lib/dataflow/propagation/install/sql-template-strings.js +9 -2
  34. package/lib/dataflow/propagation/install/string/concat.js +8 -2
  35. package/lib/dataflow/propagation/install/string/format-methods.js +7 -2
  36. package/lib/dataflow/propagation/install/string/html-methods.js +15 -5
  37. package/lib/dataflow/propagation/install/string/match.js +16 -11
  38. package/lib/dataflow/propagation/install/string/replace.js +23 -15
  39. package/lib/dataflow/propagation/install/string/slice.js +14 -6
  40. package/lib/dataflow/propagation/install/string/split.js +16 -12
  41. package/lib/dataflow/propagation/install/string/substring.js +18 -8
  42. package/lib/dataflow/propagation/install/string/trim.js +4 -1
  43. package/lib/dataflow/propagation/install/unescape.js +9 -2
  44. package/lib/dataflow/propagation/install/url/domain-parsers.js +7 -2
  45. package/lib/dataflow/propagation/install/url/index.js +1 -0
  46. package/lib/dataflow/propagation/install/url/url.js +228 -0
  47. package/lib/dataflow/propagation/install/validator/hooks.js +6 -2
  48. package/lib/dataflow/sinks/index.js +8 -4
  49. package/lib/dataflow/sinks/install/child-process.js +116 -50
  50. package/lib/dataflow/sinks/install/eval.js +138 -0
  51. package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +7 -4
  52. package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +9 -5
  53. package/lib/dataflow/sinks/install/fs.js +45 -13
  54. package/lib/dataflow/sinks/install/function.js +160 -0
  55. package/lib/dataflow/sinks/install/http/index.js +31 -0
  56. package/lib/dataflow/sinks/install/http/request.js +152 -0
  57. package/lib/dataflow/sinks/install/{http.js → http/server-response.js} +7 -4
  58. package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +8 -5
  59. package/lib/dataflow/sinks/install/marsdb.js +3 -0
  60. package/lib/dataflow/sinks/install/mongodb.js +7 -24
  61. package/lib/dataflow/sinks/install/mssql.js +49 -29
  62. package/lib/dataflow/sinks/install/mysql.js +9 -4
  63. package/lib/dataflow/sinks/install/postgres.js +6 -3
  64. package/lib/dataflow/sinks/install/sequelize.js +7 -5
  65. package/lib/dataflow/sinks/install/sqlite3.js +7 -3
  66. package/lib/dataflow/sinks/install/vm.js +276 -0
  67. package/lib/dataflow/sources/handler.js +2 -1
  68. package/lib/dataflow/sources/install/http.js +1 -1
  69. package/lib/dataflow/tag-utils.js +95 -2
  70. package/lib/dataflow/tracker.js +6 -6
  71. package/lib/index.js +2 -0
  72. package/lib/response-scanning/handlers/utils.js +2 -2
  73. package/lib/session-configuration/index.js +34 -0
  74. package/lib/session-configuration/install/http.js +79 -0
  75. package/package.json +2 -2
@@ -32,7 +32,7 @@ const { createSubsetTags } = require('../../../tag-utils');
32
32
 
33
33
  const ruleId = 'unvalidated-redirect';
34
34
 
35
- module.exports = function (core) {
35
+ module.exports = function(core) {
36
36
  const {
37
37
  depHooks,
38
38
  patcher,
@@ -59,7 +59,7 @@ module.exports = function (core) {
59
59
  URL_ENCODED,
60
60
  ];
61
61
 
62
- unvalidatedRedirect.install = function () {
62
+ unvalidatedRedirect.install = function() {
63
63
  depHooks.resolve({ name: 'express', file: 'lib/response' }, (Response) => {
64
64
  const name = 'Express.Response.location';
65
65
  patcher.patch(Response, 'location', {
@@ -81,7 +81,7 @@ module.exports = function (core) {
81
81
 
82
82
  let urlPathTags = strInfo.tags;
83
83
  if (url.indexOf('?') > -1) {
84
- urlPathTags = createSubsetTags(strInfo.tags, 0, url.indexOf('?'));
84
+ urlPathTags = createSubsetTags(strInfo.tags, 0, url.indexOf('?') + 1);
85
85
  }
86
86
 
87
87
  if (urlPathTags && isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
@@ -92,7 +92,9 @@ module.exports = function (core) {
92
92
  }],
93
93
  context: `response.location(${inspect(strInfo.value)})`,
94
94
  history: [strInfo],
95
- name: 'Express.Response.location',
95
+ name,
96
+ moduleName: 'express',
97
+ methodName: 'Response.location',
96
98
  object: {
97
99
  tracked: false,
98
100
  value: 'Express.Response',
@@ -105,6 +107,7 @@ module.exports = function (core) {
105
107
  source: 'P0',
106
108
  stacktraceOpts: {
107
109
  constructorOpt: data.hooked,
110
+ prependFrames: [data.orig]
108
111
  },
109
112
  });
110
113
 
@@ -31,6 +31,7 @@ const { createSubsetTags } = require('../../../tag-utils');
31
31
  const { filterSafeTags, patchType } = require('../../common');
32
32
 
33
33
  const ruleId = 'unvalidated-redirect';
34
+
34
35
  const getURLArgument = (args) => {
35
36
  if (!Array.isArray(args)) {
36
37
  return { index: null, url: undefined };
@@ -50,7 +51,7 @@ const getURLArgument = (args) => {
50
51
  };
51
52
  };
52
53
 
53
- module.exports = function (core) {
54
+ module.exports = function(core) {
54
55
  const {
55
56
  config,
56
57
  depHooks,
@@ -77,9 +78,9 @@ module.exports = function (core) {
77
78
  URL_ENCODED,
78
79
  ];
79
80
 
80
- unvalidatedRedirect.install = function () {
81
+ unvalidatedRedirect.install = function() {
81
82
  const name = 'fastify.Reply.prototype.redirect';
82
- depHooks.resolve({ name: 'fastify', file: 'lib/reply' }, (Reply, version) => {
83
+ depHooks.resolve({ name: 'fastify', file: 'lib/reply' }, (Reply) => {
83
84
  patcher.patch(Reply.prototype, 'redirect', {
84
85
  name,
85
86
  patchType,
@@ -98,7 +99,7 @@ module.exports = function (core) {
98
99
  let urlPathTags = strInfo.tags;
99
100
  const urlPathEndIdx = url.indexOf('?');
100
101
  if (urlPathEndIdx > -1) {
101
- urlPathTags = createSubsetTags(strInfo.tags, 0, urlPathEndIdx);
102
+ urlPathTags = createSubsetTags(strInfo.tags, 0, urlPathEndIdx + 1);
102
103
  }
103
104
 
104
105
  if (isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
@@ -119,7 +120,9 @@ module.exports = function (core) {
119
120
  args,
120
121
  context: `reply.redirect(${inspect(strInfo.value)})`,
121
122
  history: [strInfo],
122
- name: 'fastify.reply.redirect',
123
+ name,
124
+ moduleName: 'fastify',
125
+ methodName: 'reply.redirect',
123
126
  object: {
124
127
  tracked: false,
125
128
  value: 'fastify.Reply',
@@ -132,6 +135,7 @@ module.exports = function (core) {
132
135
  source: `P${valueIndex}`,
133
136
  stacktraceOpts: {
134
137
  constructorOpt: data.hooked,
138
+ prependFrames: [data.orig]
135
139
  },
136
140
  });
137
141
 
@@ -15,9 +15,22 @@
15
15
 
16
16
  'use strict';
17
17
  const { patchType } = require('../common');
18
- const { FS_METHODS, Rule, isString, DataflowTag: { URL_ENCODED, LIMITED_CHARS, ALPHANUM_SPACE_HYPHEN, SAFE_PATH, UNTRUSTED } } = require('@contrast/common');
18
+ const {
19
+ FS_METHODS,
20
+ Rule,
21
+ isString,
22
+ DataflowTag: {
23
+ URL_ENCODED,
24
+ LIMITED_CHARS,
25
+ ALPHANUM_SPACE_HYPHEN,
26
+ SAFE_PATH,
27
+ UNTRUSTED,
28
+ },
29
+ inspect,
30
+ join,
31
+ } = require('@contrast/common');
19
32
 
20
- module.exports = function (core) {
33
+ module.exports = function(core) {
21
34
  const {
22
35
  depHooks,
23
36
  patcher,
@@ -31,7 +44,12 @@ module.exports = function (core) {
31
44
  },
32
45
  } = core;
33
46
 
34
- const safeTags = [URL_ENCODED, LIMITED_CHARS, ALPHANUM_SPACE_HYPHEN, SAFE_PATH];
47
+ const safeTags = [
48
+ URL_ENCODED,
49
+ LIMITED_CHARS,
50
+ ALPHANUM_SPACE_HYPHEN,
51
+ SAFE_PATH,
52
+ ];
35
53
 
36
54
  function getValues(indices, args) {
37
55
  return indices.reduce((acc, idx) => {
@@ -41,33 +59,47 @@ module.exports = function (core) {
41
59
  }, []);
42
60
  }
43
61
 
44
- const pre = (name, indices) => (data) => {
62
+ const pre = (name, method, moduleName = 'fs', fullMethodName = '') => (data) => {
63
+ const { name: methodName, indices } = method;
45
64
  const store = sources.getStore()?.assess;
46
65
  if (!store) return;
47
66
 
48
67
  const values = getValues(indices, data.args);
49
68
  if (!values.length) return;
50
69
 
51
- const args = [];
70
+ const args = values.map((v) => {
71
+ const strInfo = tracker.getData(v);
72
+ return {
73
+ value: strInfo ? strInfo.value : v,
74
+ tracked: !!strInfo,
75
+ strInfo
76
+ };
77
+ });
52
78
  for (let i = 0; i < values.length; i++) {
53
- const strInfo = tracker.getData(values[i]);
54
- args.push({ value: strInfo ? strInfo.value : values[i], isTracked: !!strInfo });
79
+ const { strInfo } = args[i];
55
80
  if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
56
81
  continue;
57
82
  }
58
83
 
59
84
  const event = createSinkEvent({
60
85
  name,
86
+ moduleName,
87
+ methodName: fullMethodName || methodName,
88
+ context: `${name}(${join(
89
+ args.map((a) => inspect(a.value)),
90
+ ', '
91
+ )})`,
61
92
  history: [strInfo],
62
93
  object: {
63
94
  value: 'fs',
64
- isTracked: false,
95
+ tracked: false,
65
96
  },
66
- args,
97
+ args: args.map(({ value, tracked }) => ({ value, tracked })),
67
98
  tags: strInfo.tags,
68
99
  source: `P${i}`,
69
100
  stacktraceOpts: {
70
101
  contructorOpt: data.hooked,
102
+ prependFrames: [data.orig],
71
103
  },
72
104
  });
73
105
 
@@ -90,7 +122,7 @@ module.exports = function (core) {
90
122
  patcher.patch(fs, method.name, {
91
123
  name,
92
124
  patchType,
93
- pre: pre(name, method.indices)
125
+ pre: pre(name, method),
94
126
  });
95
127
  }
96
128
 
@@ -101,7 +133,7 @@ module.exports = function (core) {
101
133
  patcher.patch(fs, syncName, {
102
134
  name,
103
135
  patchType,
104
- pre: pre(name, method.indices)
136
+ pre: pre(name, method, 'fs', syncName),
105
137
  });
106
138
  }
107
139
  }
@@ -111,7 +143,7 @@ module.exports = function (core) {
111
143
  patcher.patch(fs.promises, method.name, {
112
144
  name,
113
145
  patchType,
114
- pre: pre(name, method.indices)
146
+ pre: pre(name, method, 'fs.promises'),
115
147
  });
116
148
  }
117
149
  }
@@ -124,7 +156,7 @@ module.exports = function (core) {
124
156
  patcher.patch(fsPromises, method.name, {
125
157
  name,
126
158
  patchType,
127
- pre: pre(name, method.indices)
159
+ pre: pre(name, method, 'fsPromises'),
128
160
  });
129
161
  }
130
162
  }
@@ -0,0 +1,160 @@
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 {
19
+ isString,
20
+ inspect,
21
+ join,
22
+ DataflowTag: {
23
+ UNTRUSTED,
24
+ CUSTOM_ENCODED_TRUST_BOUNDARY_VIOLATION,
25
+ CUSTOM_ENCODED,
26
+ CUSTOM_VALIDATED_TRUST_BOUNDARY_VIOLATION,
27
+ CUSTOM_VALIDATED,
28
+ LIMITED_CHARS,
29
+ },
30
+ } = require('@contrast/common');
31
+ const { patchType, filterSafeTags } = require('../common');
32
+
33
+ const ruleId = 'unsafe-code-execution';
34
+ const safeTags = [
35
+ CUSTOM_ENCODED_TRUST_BOUNDARY_VIOLATION,
36
+ CUSTOM_ENCODED,
37
+ CUSTOM_VALIDATED_TRUST_BOUNDARY_VIOLATION,
38
+ CUSTOM_VALIDATED,
39
+ LIMITED_CHARS,
40
+ ];
41
+
42
+ module.exports = function(core) {
43
+ const {
44
+ config,
45
+ logger,
46
+ patcher,
47
+ scopes: { sources, instrumentation },
48
+ assess: {
49
+ dataflow: {
50
+ tracker,
51
+ sinks: { isVulnerable, reportFindings, reportSafePositive },
52
+ eventFactory: { createSinkEvent },
53
+ },
54
+ },
55
+ } = core;
56
+
57
+ core.assess.dataflow.sinks.contrastFunction = {
58
+ install() {
59
+ if (!global.ContrastMethods?.Function) {
60
+ logger.error(
61
+ 'Cannot install `Function` instrumentation - Contrast method DNE'
62
+ );
63
+ return;
64
+ }
65
+
66
+ Object.assign(global.ContrastMethods, {
67
+ Function: patcher.patch(global.ContrastMethods.Function, {
68
+ name: 'global.ContrastMethods.Function',
69
+ patchType,
70
+ pre({ args: origArgs, hooked, orig, name }) {
71
+ const store = sources.getStore()?.assess;
72
+ const fnBody = origArgs[origArgs.length - 1];
73
+ if (
74
+ !store ||
75
+ instrumentation.isLocked() ||
76
+ !fnBody ||
77
+ !isString(fnBody)
78
+ )
79
+ return;
80
+
81
+ const strInfo = tracker.getData(fnBody);
82
+
83
+ if (!strInfo) return;
84
+
85
+ const isArgVulnerable = isVulnerable(UNTRUSTED, safeTags, strInfo.tags);
86
+
87
+ if (!isArgVulnerable && config.assess.safe_positives.enable) {
88
+ const foundSafeTags = filterSafeTags(safeTags, strInfo);
89
+ const safeStrInfo = {
90
+ value: strInfo.value,
91
+ tags: strInfo.tags,
92
+ };
93
+
94
+ reportSafePositive({
95
+ name,
96
+ ruleId,
97
+ safeTags: foundSafeTags,
98
+ strInfo: safeStrInfo,
99
+ });
100
+ }
101
+
102
+ if (isArgVulnerable) {
103
+ const args = origArgs.map((arg, idx) => {
104
+ if (idx === origArgs.length - 1) {
105
+ return {
106
+ value: strInfo.value,
107
+ tracked: true,
108
+ inspectedValue: `'${strInfo.value}'`,
109
+ };
110
+ }
111
+
112
+ const inspectedValue = inspect(arg);
113
+ const argInfo = arg && isString(arg) ? tracker.getData(arg) : null;
114
+
115
+ return {
116
+ value: argInfo ? argInfo.value : inspectedValue,
117
+ tracked: !!argInfo,
118
+ inspectedValue
119
+ };
120
+ });
121
+ const event = createSinkEvent({
122
+ name,
123
+ context: `${name}(${join(
124
+ args.map((a) => a.inspectedValue),
125
+ ', '
126
+ )})`,
127
+ history: [strInfo],
128
+ object: {
129
+ value: 'global.ContrastMethods',
130
+ tracked: false,
131
+ },
132
+ moduleName: 'global.ContrastMethods',
133
+ methodName: 'Function',
134
+ args: args.map((a) => ({
135
+ value: a.value,
136
+ tracked: a.tracked,
137
+ })),
138
+ tags: strInfo.tags,
139
+ source: `P${origArgs.length - 1}`,
140
+ stacktraceOpts: {
141
+ contructorOpt: hooked,
142
+ prependFrames: [orig],
143
+ },
144
+ });
145
+
146
+ if (event) {
147
+ reportFindings({
148
+ ruleId,
149
+ sinkEvent: event,
150
+ });
151
+ }
152
+ }
153
+ },
154
+ }),
155
+ });
156
+ },
157
+ };
158
+
159
+ return core.assess.dataflow.sinks.contrastFunction;
160
+ };
@@ -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
+ const http = core.assess.dataflow.sinks.http = {};
22
+
23
+ require('./request')(core);
24
+ require('./server-response')(core);
25
+
26
+ http.install = function() {
27
+ callChildComponentMethodsSync(http, 'install');
28
+ };
29
+
30
+ return http;
31
+ };
@@ -0,0 +1,152 @@
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 {
19
+ inspect,
20
+ isString,
21
+ DataflowTag: {
22
+ UNTRUSTED,
23
+ CUSTOM_ENCODED_SSRF,
24
+ CUSTOM_ENCODED,
25
+ CUSTOM_VALIDATED_SSRF,
26
+ CUSTOM_VALIDATED,
27
+ LIMITED_CHARS
28
+ }
29
+ } = require('@contrast/common');
30
+ const Url = require('url');
31
+ const trustedLibs = [/^(?!.*(newrelic)).*http.*$/];
32
+ const { patchType } = require('../../common');
33
+ const { createAppendTags } = require('../../../tag-utils');
34
+
35
+ module.exports = function(core) {
36
+ const {
37
+ depHooks,
38
+ patcher,
39
+ scopes: { sources },
40
+ assess: {
41
+ dataflow: {
42
+ tracker,
43
+ sinks: {
44
+ isVulnerable,
45
+ reportFindings
46
+ },
47
+ eventFactory: { createSinkEvent },
48
+ },
49
+ },
50
+ } = core;
51
+ const http = core.assess.dataflow.sinks.http.request = {};
52
+
53
+ const safeTags = [
54
+ CUSTOM_ENCODED_SSRF,
55
+ CUSTOM_ENCODED,
56
+ CUSTOM_VALIDATED_SSRF,
57
+ CUSTOM_VALIDATED,
58
+ LIMITED_CHARS
59
+ ];
60
+
61
+ function getValueFromReq(value, key) {
62
+ if (isString(value)) {
63
+ const url = new Url.URL(value);
64
+ if (isString(url[key])) {
65
+ return url[key];
66
+ }
67
+ }
68
+
69
+ if (isString(value[key])) {
70
+ return value[key];
71
+ }
72
+ }
73
+
74
+ function containsTrustedLib(stack) {
75
+ for (const { file } of stack) {
76
+ for (const trusted of trustedLibs) {
77
+ if (trusted.exec(file)) {
78
+ return true;
79
+ }
80
+ }
81
+ }
82
+
83
+ return false;
84
+ }
85
+
86
+ http.install = function() {
87
+ ['http', 'https'].forEach((moduleName) => {
88
+ depHooks.resolve({ name: moduleName }, (module) => {
89
+ const name = `${moduleName}.request`;
90
+ const methodName = 'request';
91
+ patcher.patch(module, methodName, {
92
+ name,
93
+ patchType,
94
+ pre(data) {
95
+ const sourceContext = sources.getStore()?.assess;
96
+ if (!sourceContext) return;
97
+
98
+ const [req] = data.args;
99
+ if (!req) return;
100
+
101
+ ['host', 'hostname', 'localAddress', 'protocol'].forEach((key) => {
102
+ const value = getValueFromReq(req, key);
103
+ if (!value) return;
104
+
105
+ const strInfo = tracker.getData(value);
106
+ if (!strInfo) return;
107
+
108
+ if (containsTrustedLib(strInfo.stack)) return;
109
+
110
+ const arg0 = isString(req) ? req : inspect(req);
111
+ const idx = arg0.indexOf(value);
112
+ const urlTags = createAppendTags({}, strInfo.tags, idx);
113
+
114
+ if (urlTags && isVulnerable(UNTRUSTED, safeTags, urlTags)) {
115
+ const event = createSinkEvent({
116
+ name,
117
+ moduleName,
118
+ methodName: 'request',
119
+ context: `${moduleName}.request(${arg0})`,
120
+ history: [strInfo],
121
+ object: {
122
+ tracked: false,
123
+ value: moduleName
124
+ },
125
+ args: [{
126
+ value: arg0,
127
+ tracked: true
128
+ }],
129
+ source: 'P0',
130
+ stacktraceOpts: {
131
+ constructorOpt: data.hooked,
132
+ prependFrames: [data.orig]
133
+ },
134
+ tags: urlTags,
135
+ });
136
+
137
+ if (event) {
138
+ reportFindings({
139
+ ruleId: 'ssrf',
140
+ sinkEvent: event
141
+ });
142
+ }
143
+ }
144
+ });
145
+ }
146
+ });
147
+ });
148
+ });
149
+ };
150
+
151
+ return http;
152
+ };
@@ -31,7 +31,7 @@ const {
31
31
  },
32
32
  Rule: { REFLECTED_XSS: ruleId },
33
33
  } = require('@contrast/common');
34
- const { patchType, filterSafeTags } = require('../common');
34
+ const { patchType, filterSafeTags } = require('../../common');
35
35
 
36
36
  module.exports = function(core) {
37
37
  const {
@@ -52,7 +52,7 @@ module.exports = function(core) {
52
52
  },
53
53
  },
54
54
  } = core;
55
- const http = core.assess.dataflow.sinks.http = {};
55
+ const http = core.assess.dataflow.sinks.http.serverResponse = {};
56
56
 
57
57
  const safeTags = [
58
58
  ALPHANUM_SPACE_HYPHEN,
@@ -86,9 +86,11 @@ module.exports = function(core) {
86
86
  value: strInfo.value,
87
87
  tracked: true
88
88
  }],
89
- context: `res.${method}(${strInfo.value})`,
89
+ context: `res.${method}('${strInfo.value}')`,
90
90
  history: [strInfo],
91
91
  name,
92
+ moduleName: 'http',
93
+ methodName: `ServerResponse.prototype.${method}`,
92
94
  object: {
93
95
  tracked: false,
94
96
  value: 'http.ServerResponse'
@@ -99,7 +101,8 @@ module.exports = function(core) {
99
101
  },
100
102
  source: 'P0',
101
103
  stacktraceOpts: {
102
- constructorOpt: data.hooked
104
+ constructorOpt: data.hooked,
105
+ prependFrames: [data.orig]
103
106
  },
104
107
  tags: strInfo.tags,
105
108
  });
@@ -32,7 +32,7 @@ const { filterSafeTags, patchType } = require('../../common');
32
32
 
33
33
  const ruleId = 'unvalidated-redirect';
34
34
 
35
- module.exports = function (core) {
35
+ module.exports = function(core) {
36
36
  const {
37
37
  depHooks,
38
38
  patcher,
@@ -59,11 +59,11 @@ module.exports = function (core) {
59
59
  URL_ENCODED,
60
60
  ];
61
61
 
62
- unvalidatedRedirect.install = function () {
62
+ unvalidatedRedirect.install = function() {
63
63
  depHooks.resolve({ name: 'koa', file: 'lib/response', version: '<2.9.0' }, (Response) => {
64
64
  const name = 'Koa.Response.redirect';
65
65
  patcher.patch(Response, 'redirect', {
66
- name: 'Koa.Response.redirect',
66
+ name,
67
67
  patchType,
68
68
  pre(data) {
69
69
  const assessStore = sources.getStore()?.assess;
@@ -84,7 +84,7 @@ module.exports = function (core) {
84
84
  let urlPathTags = strInfo.tags;
85
85
 
86
86
  if (url.indexOf('?') > -1) {
87
- urlPathTags = createSubsetTags(strInfo.tags, 0, url.indexOf('?'));
87
+ urlPathTags = createSubsetTags(strInfo.tags, 0, url.indexOf('?') + 1);
88
88
  }
89
89
 
90
90
  if (urlPathTags && isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
@@ -104,7 +104,9 @@ module.exports = function (core) {
104
104
  args,
105
105
  context: `response.redirect(${inspect(strInfo.value)})`,
106
106
  history: [strInfo],
107
- name: 'Koa.Response.redirect',
107
+ name,
108
+ moduleName: 'koa',
109
+ methodName: 'Response.redirect',
108
110
  object: {
109
111
  tracked: false,
110
112
  value: 'Koa.Response',
@@ -117,6 +119,7 @@ module.exports = function (core) {
117
119
  source: `P${isBackRoute ? 1 : 0}`,
118
120
  stacktraceOpts: {
119
121
  constructorOpt: data.hooked,
122
+ prependFrames: [data.orig]
120
123
  },
121
124
  });
122
125
 
@@ -102,6 +102,8 @@ module.exports = function(core) {
102
102
  const sinkEvent = createSinkEvent({
103
103
  args,
104
104
  context: `marsdb.Collection.${method}(${args.map((a) => a.value)})`,
105
+ moduleName: 'marsdb',
106
+ methodName: `Collection.prototype.${method}`,
105
107
  history: [strInfo],
106
108
  object: {
107
109
  tracked: false,
@@ -112,6 +114,7 @@ module.exports = function(core) {
112
114
  source: `P${argIdx}`,
113
115
  stacktraceOpts: {
114
116
  constructorOpt: data.hooked,
117
+ prependFrames: [data.orig]
115
118
  },
116
119
  tags: strInfo.tags,
117
120
  });