@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
@@ -15,7 +15,6 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const util = require('util');
19
18
  const {
20
19
  DataflowTag: {
21
20
  UNTRUSTED,
@@ -28,7 +27,8 @@ const {
28
27
  Rule: { NOSQL_INJECTION_MONGO },
29
28
  isNonEmptyObject,
30
29
  traverseValues,
31
- isString
30
+ isString,
31
+ inspect
32
32
  } = require('@contrast/common');
33
33
  const utils = require('../../tag-utils');
34
34
  const { patchType, filterSafeTags } = require('../common');
@@ -82,7 +82,6 @@ module.exports = function(core) {
82
82
  }
83
83
  } = core;
84
84
 
85
- const inspect = patcher.unwrap(util.inspect);
86
85
  const instr = core.assess.dataflow.sinks.mongodb = {};
87
86
 
88
87
  instr.getQueryVulnerabilityInfo = function getQueryVulnerabilityInfo(query) {
@@ -256,7 +255,7 @@ module.exports = function(core) {
256
255
  if (safeReports.length && config.assess.safe_positives.enable) {
257
256
  const safeTags = safeReports.map((report) => filterSafeTags(querySafeTags, report.strInfo));
258
257
  const strInfo = safeReports.map((report) => {
259
- const tags = report.path ? getAdjustedQueryTags(report.path, report.strInfo, inspect(origArgs[report.argIdx], { depth: 4 })) : report.strInfo?.tags;
258
+ const tags = report.path ? utils.createAdjustedQueryTags(report.path, report.strInfo.tags, report.strInfo.value, inspect(origArgs[report.argIdx], { depth: 4 })) : report.strInfo?.tags;
260
259
 
261
260
  return {
262
261
  value: inspect(origArgs[report.argIdx], { depth: 4 }),
@@ -282,17 +281,19 @@ module.exports = function(core) {
282
281
  tracked: idx === vulnArgIdx,
283
282
  }));
284
283
 
285
- const tags = path ? getAdjustedQueryTags(path, strInfo, args[vulnArgIdx].value) : strInfo?.tags;
284
+ const tags = path ? utils.createAdjustedQueryTags(path, strInfo.tags, strInfo.value, args[vulnArgIdx].value) : strInfo?.tags;
286
285
  const resultVal = args[args.length - 1].value.startsWith('[Function') ? '' : 'Promise';
287
286
  const sinkEvent = createSinkEvent({
288
287
  args,
289
- context: `${objName}.${method}(${args.map((a) => a.value)})`,
288
+ context: `${objName}.${method}(${args.map((a, idx) => isString(origArgs[idx]) ? `'${a.value}'` : a.value)})`,
290
289
  history: [strInfo],
291
290
  object: {
292
291
  tracked: false,
293
292
  value: `mongodb.${entity}`,
294
293
  },
295
294
  name,
295
+ moduleName: 'mongodb',
296
+ methodName: `${entity}.prototype.${method}`,
296
297
  result: {
297
298
  tracked: false,
298
299
  value: resultVal,
@@ -399,22 +400,4 @@ module.exports = function(core) {
399
400
 
400
401
  return name;
401
402
  }
402
-
403
- function getAdjustedQueryTags(path, strInfo, argString) {
404
- const { tags } = strInfo;
405
- let idx = -1;
406
- for (const str of [...path, strInfo.value]) {
407
- // This is the case with the `.aggregate` method
408
- // where the argument is an array
409
- if (str === 0) continue;
410
-
411
- idx = argString.indexOf(str, idx);
412
- if (idx == -1) {
413
- idx = -1;
414
- break;
415
- }
416
- }
417
-
418
- return idx > 0 ? utils.createAppendTags([], tags, idx) : strInfo.tags;
419
- }
420
403
  };
@@ -21,7 +21,7 @@ const {
21
21
  isString
22
22
  } = require('@contrast/common');
23
23
  const { createModuleLabel } = require('../../propagation/common');
24
- const { patchType } = require('../common');
24
+ const { patchType, filterSafeTags } = require('../common');
25
25
 
26
26
  const safeTags = [
27
27
  SQL_ENCODED,
@@ -30,21 +30,24 @@ const safeTags = [
30
30
  CUSTOM_ENCODED,
31
31
  ];
32
32
 
33
- module.exports = function (core) {
33
+ const ruleId = SQL_INJECTION;
34
+
35
+ module.exports = function(core) {
34
36
  const {
35
37
  depHooks,
36
38
  patcher,
39
+ config,
37
40
  scopes: { sources },
38
41
  assess: {
39
42
  dataflow: {
40
43
  tracker,
41
- sinks: { isVulnerable, isLocked, reportFindings },
44
+ sinks: { isVulnerable, isLocked, reportFindings, reportSafePositive },
42
45
  eventFactory: { createSinkEvent },
43
46
  },
44
47
  },
45
48
  } = core;
46
49
 
47
- const pre = (name, obj, version) => (data) => {
50
+ const pre = (name, method, obj, version) => (data) => {
48
51
  const store = sources.getStore()?.assess;
49
52
  if (
50
53
  !store ||
@@ -54,34 +57,48 @@ module.exports = function (core) {
54
57
  ) return;
55
58
 
56
59
  const strInfo = tracker.getData(data.args[0]);
57
- if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
58
- return;
59
- }
60
+ if (!strInfo) return;
60
61
 
61
- const event = createSinkEvent({
62
- name,
63
- history: [strInfo],
64
- object: {
65
- value: `[${createModuleLabel('mssql', version)}].${obj}`,
66
- tracked: false,
67
- },
68
- args: [
69
- {
70
- value: strInfo.value,
71
- tracked: true,
62
+ if (isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
63
+ const event = createSinkEvent({
64
+ name,
65
+ moduleName: 'mssql',
66
+ methodName: `PreparedStatement.prototype.${method}`,
67
+ context: `mssql.PreparedStatement.${method}('${strInfo.value}')`,
68
+ history: [strInfo],
69
+ object: {
70
+ value: `[${createModuleLabel('mssql', version)}].${obj}`,
71
+ tracked: false,
72
72
  },
73
- ],
74
- tags: strInfo.tags,
75
- source: 'P0',
76
- stacktraceOpts: {
77
- contructorOpt: data.hooked,
78
- },
79
- });
73
+ args: [
74
+ {
75
+ value: strInfo.value,
76
+ tracked: true,
77
+ },
78
+ ],
79
+ tags: strInfo.tags,
80
+ source: 'P0',
81
+ stacktraceOpts: {
82
+ contructorOpt: data.hooked,
83
+ prependFrames: [data.orig]
84
+ },
85
+ });
80
86
 
81
- if (event) {
82
- reportFindings({
83
- ruleId: SQL_INJECTION,
84
- sinkEvent: event,
87
+ if (event) {
88
+ reportFindings({
89
+ ruleId: SQL_INJECTION,
90
+ sinkEvent: event,
91
+ });
92
+ }
93
+ } else if (config.assess.safe_positives.enable) {
94
+ reportSafePositive({
95
+ name,
96
+ ruleId,
97
+ safeTags: filterSafeTags(safeTags, strInfo),
98
+ strInfo: {
99
+ tags: strInfo.tags,
100
+ value: strInfo.value,
101
+ }
85
102
  });
86
103
  }
87
104
  };
@@ -96,6 +113,7 @@ module.exports = function (core) {
96
113
  patchType,
97
114
  pre: pre(
98
115
  'mssql/lib/base/prepared-statement.prototype.prepare',
116
+ 'prepare',
99
117
  'PreparedStatement',
100
118
  version,
101
119
  ),
@@ -111,6 +129,7 @@ module.exports = function (core) {
111
129
  patchType,
112
130
  pre: pre(
113
131
  'mssql/lib/base/request.prototype.batch',
132
+ 'batch',
114
133
  'Request',
115
134
  version,
116
135
  ),
@@ -121,6 +140,7 @@ module.exports = function (core) {
121
140
  patchType,
122
141
  pre: pre(
123
142
  'mssql/lib/base/request.prototype.query',
143
+ 'query',
124
144
  'Request',
125
145
  version,
126
146
  ),
@@ -28,6 +28,7 @@ const {
28
28
  LIMITED_CHARS,
29
29
  UNTRUSTED
30
30
  },
31
+ inspect
31
32
  } = require('@contrast/common');
32
33
 
33
34
  const safeTags = [
@@ -39,7 +40,7 @@ const safeTags = [
39
40
  LIMITED_CHARS,
40
41
  ];
41
42
 
42
- module.exports = function (core) {
43
+ module.exports = function(core) {
43
44
  const {
44
45
  depHooks,
45
46
  patcher,
@@ -63,7 +64,7 @@ module.exports = function (core) {
63
64
  }
64
65
  }
65
66
 
66
- const pre = (module, file, obj) => (data) => {
67
+ const pre = (module, file, obj, method) => (data) => {
67
68
  const store = sources.getStore()?.assess;
68
69
  if (
69
70
  !store ||
@@ -81,6 +82,9 @@ module.exports = function (core) {
81
82
 
82
83
  const event = createSinkEvent({
83
84
  name: `${module}/${file}`,
85
+ moduleName: module,
86
+ methodName: `prototype.${method}`,
87
+ context: `${module}.${method}(${inspect(data.args[0])})`,
84
88
  history: [strInfo],
85
89
  object: {
86
90
  value: `${module}.${obj}`,
@@ -96,6 +100,7 @@ module.exports = function (core) {
96
100
  source: 'P0',
97
101
  stacktraceOpts: {
98
102
  contructorOpt: data.hooked,
103
+ prependFrames: [data.orig]
99
104
  },
100
105
  });
101
106
 
@@ -115,7 +120,7 @@ module.exports = function (core) {
115
120
  patcher.patch(Connection.prototype, 'query', {
116
121
  name: 'Connection.prototype.query',
117
122
  patchType,
118
- pre: pre('mysql', 'lib/Connection.query', 'Connection')
123
+ pre: pre('mysql', 'lib/Connection.query', 'Connection', 'query')
119
124
  });
120
125
  },
121
126
  );
@@ -126,7 +131,7 @@ module.exports = function (core) {
126
131
  patcher.patch(connection.prototype, `${method}`, {
127
132
  name: `connection.prototype.${method}`,
128
133
  patchType,
129
- pre: pre('mysql2', `lib/connection.Connection.${method}`, 'connection')
134
+ pre: pre('mysql2', `lib/connection.Connection.${method}`, 'connection', method)
130
135
  });
131
136
  });
132
137
  },
@@ -19,11 +19,11 @@ const util = require('util');
19
19
  const {
20
20
  DataflowTag: { UNTRUSTED, SQL_ENCODED, LIMITED_CHARS, CUSTOM_VALIDATED, CUSTOM_ENCODED },
21
21
  Rule: { SQL_INJECTION: ruleId },
22
- isString
22
+ isString,
23
23
  } = require('@contrast/common');
24
24
  const { filterSafeTags, patchType } = require('../common');
25
25
 
26
- module.exports = function (core) {
26
+ module.exports = function(core) {
27
27
  const {
28
28
  config,
29
29
  depHooks,
@@ -74,6 +74,8 @@ module.exports = function (core) {
74
74
  context: `${objValue}.query(${arg0Val})`,
75
75
  history: [strInfo],
76
76
  name: methodSignature,
77
+ moduleName: `${methodSignature.includes('pool') ? 'pg-pool' : 'pg'}`,
78
+ methodName: 'Connection.prototype.query',
77
79
  object: {
78
80
  tracked: false,
79
81
  value: objValue,
@@ -86,6 +88,7 @@ module.exports = function (core) {
86
88
  source: 'P0',
87
89
  stacktraceOpts: {
88
90
  constructorOpt: data.hooked,
91
+ prependFrames: [data.orig]
89
92
  },
90
93
  });
91
94
 
@@ -108,7 +111,7 @@ module.exports = function (core) {
108
111
  }
109
112
  };
110
113
 
111
- postgres.install = function () {
114
+ postgres.install = function() {
112
115
  const pgClientQueryPatchName = 'pg.Client.prototype.query';
113
116
  depHooks.resolve(
114
117
  { name: 'pg', file: 'lib/client.js' },
@@ -28,7 +28,7 @@ const {
28
28
  } = require('@contrast/common');
29
29
  const { patchType, filterSafeTags } = require('../common');
30
30
 
31
- module.exports = function (core) {
31
+ module.exports = function(core) {
32
32
  const {
33
33
  depHooks,
34
34
  patcher,
@@ -54,7 +54,7 @@ module.exports = function (core) {
54
54
 
55
55
  const sequelize = (core.assess.dataflow.sinks.sequelize = {});
56
56
 
57
- sequelize.install = function () {
57
+ sequelize.install = function() {
58
58
  const sequelizeQueryPatchName = 'sequelize.prototype.query';
59
59
  depHooks.resolve({ name: 'sequelize' }, (sequelize) => {
60
60
  patcher.patch(sequelize.prototype, 'query', {
@@ -69,7 +69,7 @@ module.exports = function (core) {
69
69
 
70
70
  try {
71
71
  const queryInfo = tracker.getData(query);
72
- const isVulnerableQuery = isVulnerable(requiredTag, safeTags, queryInfo.tags);
72
+ const isVulnerableQuery = queryInfo && isVulnerable(requiredTag, safeTags, queryInfo.tags);
73
73
 
74
74
  if (queryInfo && !isVulnerableQuery && config.assess.safe_positives.enable) {
75
75
  reportSafePositive({
@@ -94,8 +94,8 @@ module.exports = function (core) {
94
94
  typeof args[0] === 'string' ? args[0] : inspect(args[0]);
95
95
  const inspectedOptions = args[1] ? inspect(args[1]) : '';
96
96
  const contextArgs = args[1]
97
- ? `${sqlValue}, ${inspectedOptions}`
98
- : sqlValue;
97
+ ? `'${sqlValue}', ${inspectedOptions}`
98
+ : `'${sqlValue}'`;
99
99
 
100
100
  const reportedArgs = [{ value: sqlValue, tracked: true }];
101
101
  args[1] &&
@@ -104,6 +104,8 @@ module.exports = function (core) {
104
104
  const event = createSinkEvent({
105
105
  context: `sequelize.prototype.query(${contextArgs})`,
106
106
  name: sequelizeQueryPatchName,
107
+ moduleName: 'sequelize',
108
+ methodName: 'prototype.query',
107
109
  history: [queryInfo],
108
110
  object: {
109
111
  value: 'sequelize.prototype',
@@ -29,7 +29,7 @@ const safeTags = [
29
29
  CUSTOM_ENCODED,
30
30
  ];
31
31
 
32
- module.exports = function (core) {
32
+ module.exports = function(core) {
33
33
  const {
34
34
  depHooks,
35
35
  patcher,
@@ -43,7 +43,7 @@ module.exports = function (core) {
43
43
  },
44
44
  } = core;
45
45
 
46
- const pre = (name) => (data) => {
46
+ const pre = (name, method) => (data) => {
47
47
  const store = sources.getStore()?.assess;
48
48
  if (
49
49
  !store ||
@@ -59,6 +59,9 @@ module.exports = function (core) {
59
59
 
60
60
  const event = createSinkEvent({
61
61
  name,
62
+ moduleName: 'sqlite3',
63
+ methodName: `Database.prototype.${method}`,
64
+ context: `db.${method}('${strInfo.value}')`,
62
65
  history: [strInfo],
63
66
  object: {
64
67
  value: '[Module<sqlite3>].Database',
@@ -74,6 +77,7 @@ module.exports = function (core) {
74
77
  source: 'P0',
75
78
  stacktraceOpts: {
76
79
  contructorOpt: data.hooked,
80
+ prependFrames: [data.orig]
77
81
  },
78
82
  });
79
83
 
@@ -93,7 +97,7 @@ module.exports = function (core) {
93
97
  patcher.patch(sqlite3.Database.prototype, method, {
94
98
  name,
95
99
  patchType,
96
- pre: pre(name)
100
+ pre: pre(name, method)
97
101
  });
98
102
  });
99
103
  });
@@ -0,0 +1,276 @@
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
+ const { patchType, filterSafeTags } = require('../common');
18
+ const {
19
+ isString,
20
+ DataflowTag: {
21
+ UNTRUSTED,
22
+ CUSTOM_ENCODED_TRUST_BOUNDARY_VIOLATION,
23
+ CUSTOM_ENCODED,
24
+ CUSTOM_VALIDATED_TRUST_BOUNDARY_VIOLATION,
25
+ CUSTOM_VALIDATED,
26
+ LIMITED_CHARS,
27
+ },
28
+ isNonEmptyObject,
29
+ inspect,
30
+ traverseValues,
31
+ join,
32
+ split,
33
+ } = require('@contrast/common');
34
+ const { createAdjustedQueryTags } = require('../../tag-utils');
35
+ const ruleId = 'unsafe-code-execution';
36
+ const safeTags = [
37
+ CUSTOM_ENCODED_TRUST_BOUNDARY_VIOLATION,
38
+ CUSTOM_ENCODED,
39
+ CUSTOM_VALIDATED_TRUST_BOUNDARY_VIOLATION,
40
+ CUSTOM_VALIDATED,
41
+ LIMITED_CHARS,
42
+ ];
43
+
44
+ module.exports = function(core) {
45
+ const {
46
+ config,
47
+ depHooks,
48
+ patcher,
49
+ scopes: { sources, instrumentation },
50
+ assess: {
51
+ dataflow: {
52
+ tracker,
53
+ sinks: {
54
+ isVulnerable,
55
+ runInActiveSink,
56
+ isLocked,
57
+ reportFindings,
58
+ reportSafePositive,
59
+ },
60
+ eventFactory: { createSinkEvent },
61
+ },
62
+ },
63
+ } = core;
64
+
65
+ function accumulateArgsInfo(origArgs, idxsToCheck) {
66
+ const argsInfo = [];
67
+ for (let i = 0; i < origArgs.length; i++) {
68
+ const arg = origArgs[i];
69
+ const infoObj = {
70
+ isStringArg: isString(arg),
71
+ idx: i,
72
+ sanitizedStrInfos: [],
73
+ sanitizedStrPaths: []
74
+ };
75
+
76
+ if (
77
+ !arg ||
78
+ !idxsToCheck.includes(i) ||
79
+ (!infoObj.isStringArg && !isNonEmptyObject(arg))
80
+ ) {
81
+ const inspectedArg = inspect(origArgs[i]);
82
+ infoObj.argsValue = { value: inspectedArg, tracked: false };
83
+ infoObj.ctxValue = inspectedArg;
84
+ argsInfo.push(infoObj);
85
+
86
+ continue;
87
+ }
88
+
89
+ if (infoObj.isStringArg) {
90
+ const strInfo = tracker.getData(arg);
91
+ infoObj.strInfo = strInfo;
92
+ infoObj.argsValue = {
93
+ value: strInfo ? strInfo.value : arg,
94
+ tracked: !!strInfo,
95
+ };
96
+ infoObj.ctxValue = `"${strInfo?.value || arg}"`;
97
+ infoObj.isVulnerableArg =
98
+ strInfo && isVulnerable(UNTRUSTED, safeTags, strInfo.tags);
99
+ if (strInfo && !infoObj.isVulnerableArg) {
100
+ infoObj.sanitizedStrInfos.push(strInfo);
101
+ infoObj.isSanitizedArg = true;
102
+ }
103
+ } else {
104
+ traverseValues(
105
+ arg,
106
+ (path, _type, value) => {
107
+ const strInfo = tracker.getData(value);
108
+ infoObj.strInfo = strInfo;
109
+ infoObj.isVulnerableArg =
110
+ strInfo && isVulnerable(UNTRUSTED, safeTags, strInfo.tags);
111
+ if (infoObj.isVulnerableArg) {
112
+ infoObj.vulnerableArgPath = [...path];
113
+ return true;
114
+ } else if (strInfo) {
115
+ infoObj.sanitizedStrInfos.push(strInfo);
116
+ infoObj.sanitizedStrPaths.push([...path]);
117
+ infoObj.isSanitizedArg = true;
118
+ }
119
+ },
120
+ 100
121
+ );
122
+
123
+ const inspectedArg = inspect(arg);
124
+
125
+ infoObj.argsValue = { value: inspectedArg, tracked: false };
126
+ infoObj.ctxValue = inspectedArg;
127
+ }
128
+
129
+ argsInfo.push(infoObj);
130
+ }
131
+
132
+ return argsInfo;
133
+ }
134
+
135
+ function around(next, { args: origArgs, hooked, orig, name }) {
136
+ const store = sources.getStore()?.assess;
137
+ if (
138
+ !store ||
139
+ instrumentation.isLocked() ||
140
+ isLocked('unsafe-code-execution')
141
+ ) {
142
+ return next();
143
+ }
144
+
145
+ const methodPath = split(name, '.');
146
+ const method = methodPath[methodPath.length - 1];
147
+ const idxsToCheck = method === 'runInNewContext' ? [0, 1] : [0];
148
+ const argsInfo = accumulateArgsInfo(origArgs, idxsToCheck);
149
+ const vulnerableArg = argsInfo.find((arg) => arg.isVulnerableArg);
150
+ const sanitizedArgs = argsInfo.filter((arg) => arg.isSanitizedArg);
151
+
152
+ if (!vulnerableArg && sanitizedArgs.length && config.assess.safe_positives.enable) {
153
+ const foundSafeTags = sanitizedArgs.reduce((a, c) => {
154
+ const tags = [];
155
+
156
+ c.sanitizedStrInfos.forEach((i) => {
157
+ tags.push(filterSafeTags(safeTags, i));
158
+ });
159
+
160
+ tags.forEach((tag) => a.add(...tag));
161
+ return a;
162
+ }, new Set());
163
+ const strInfo = sanitizedArgs.map((arg) => {
164
+ const value = inspect(origArgs[arg.idx], { depth: 4 });
165
+ const tags = [];
166
+ const strValues = [];
167
+ arg.sanitizedStrInfos.forEach((info, idx) => {
168
+ strValues.push(info.value);
169
+ if (arg.isStringArg) {
170
+ tags.push(info.tags);
171
+ } else {
172
+ tags.push(
173
+ createAdjustedQueryTags(
174
+ arg.sanitizedStrPaths[idx] || [],
175
+ info.tags,
176
+ info.value,
177
+ value
178
+ )
179
+ );
180
+ }
181
+ });
182
+
183
+ return {
184
+ value,
185
+ strValues,
186
+ tags,
187
+ };
188
+ });
189
+
190
+ reportSafePositive({
191
+ name,
192
+ ruleId,
193
+ safeTags: Array.from(foundSafeTags),
194
+ strInfo,
195
+ });
196
+
197
+ return name === 'vm.runInNewContext'
198
+ ? runInActiveSink('unsafe-code-execution', () => next())
199
+ : next();
200
+ }
201
+
202
+ if (vulnerableArg) {
203
+ const event = createSinkEvent({
204
+ name,
205
+ context: `${name}(${join(
206
+ argsInfo.map((a) => a.ctxValue),
207
+ ', '
208
+ )})`,
209
+ history: [vulnerableArg.strInfo],
210
+ object: {
211
+ value: methodPath.includes('prototype')
212
+ ? 'vm.Script.prototype'
213
+ : 'vm',
214
+ tracked: false,
215
+ },
216
+ moduleName: 'vm',
217
+ methodName: method,
218
+ args: argsInfo.map((a) => a.argsValue),
219
+ tags: vulnerableArg.vulnerableArgPath?.length
220
+ ? createAdjustedQueryTags(
221
+ vulnerableArg.vulnerableArgPath,
222
+ vulnerableArg.strInfo.tags,
223
+ vulnerableArg.strInfo.value,
224
+ vulnerableArg.argsValue.value
225
+ )
226
+ : vulnerableArg.strInfo.tags,
227
+ source: `P${vulnerableArg.idx}`,
228
+ stacktraceOpts: {
229
+ contructorOpt: hooked,
230
+ prependFrames: [orig],
231
+ },
232
+ });
233
+
234
+ if (event) {
235
+ reportFindings({
236
+ ruleId,
237
+ sinkEvent: event,
238
+ });
239
+ }
240
+ }
241
+
242
+ return name === 'vm.runInNewContext'
243
+ ? runInActiveSink('unsafe-code-execution', () => next())
244
+ : next();
245
+ }
246
+
247
+ core.assess.dataflow.sinks.vm = {
248
+ install() {
249
+ depHooks.resolve({ name: 'vm' }, (vm) => {
250
+ [
251
+ 'Script',
252
+ 'createScript',
253
+ 'runInContext',
254
+ 'runInThisContext',
255
+ 'createContext',
256
+ 'runInNewContext',
257
+ ].forEach((method) => {
258
+ const name = `vm.${method}`;
259
+ patcher.patch(vm, method, {
260
+ name,
261
+ patchType,
262
+ around,
263
+ });
264
+ });
265
+
266
+ patcher.patch(vm.Script.prototype, 'runInNewContext', {
267
+ name: 'vm.Script.prototype.runInNewContext',
268
+ patchType,
269
+ around,
270
+ });
271
+ });
272
+ },
273
+ };
274
+
275
+ return core.assess.dataflow.sinks.vm;
276
+ };
@@ -19,6 +19,7 @@ const {
19
19
  InputType,
20
20
  DataflowTag,
21
21
  isString,
22
+ join,
22
23
  } = require('@contrast/common');
23
24
 
24
25
  module.exports = function(core) {
@@ -115,7 +116,7 @@ module.exports = function(core) {
115
116
  }
116
117
 
117
118
  traverse(_data, (path, fieldName, value, obj) => {
118
- const pathName = path.join('.');
119
+ const pathName = join(path, '.');
119
120
 
120
121
  if (sourceContext.sourceEventsCount >= max) {
121
122
  core.logger.trace({ inputType, name }, 'exiting assess source handling - %s max events exceeded', max);