@contrast/assess 1.6.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 (27) hide show
  1. package/lib/dataflow/propagation/index.js +1 -0
  2. package/lib/dataflow/propagation/install/contrast-methods/string.js +5 -1
  3. package/lib/dataflow/propagation/install/encode-uri-component.js +5 -2
  4. package/lib/dataflow/propagation/install/pug-runtime-escape.js +5 -2
  5. package/lib/dataflow/propagation/install/sequelize.js +310 -0
  6. package/lib/dataflow/propagation/install/sql-template-strings.js +5 -4
  7. package/lib/dataflow/propagation/install/string/match.js +2 -2
  8. package/lib/dataflow/propagation/install/string/replace.js +9 -4
  9. package/lib/dataflow/sinks/common.js +10 -1
  10. package/lib/dataflow/sinks/index.js +30 -1
  11. package/lib/dataflow/sinks/install/express/index.js +29 -0
  12. package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +134 -0
  13. package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +96 -69
  14. package/lib/dataflow/sinks/install/http.js +20 -5
  15. package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +33 -9
  16. package/lib/dataflow/sinks/install/mongodb.js +171 -54
  17. package/lib/dataflow/sinks/install/mssql.js +9 -4
  18. package/lib/dataflow/sinks/install/mysql.js +20 -4
  19. package/lib/dataflow/sinks/install/postgres.js +25 -12
  20. package/lib/dataflow/sinks/install/sequelize.js +142 -0
  21. package/lib/dataflow/sinks/install/sqlite3.js +9 -4
  22. package/lib/dataflow/sources/handler.js +4 -0
  23. package/lib/dataflow/sources/index.js +4 -1
  24. package/lib/dataflow/sources/install/body-parser1.js +120 -0
  25. package/lib/dataflow/sources/install/cookie-parser1.js +101 -0
  26. package/lib/dataflow/sources/install/express/index.js +28 -0
  27. package/package.json +3 -3
@@ -20,16 +20,18 @@ const {
20
20
  DataflowTag: {
21
21
  UNTRUSTED,
22
22
  ALPHANUM_SPACE_HYPHEN,
23
+ CUSTOM_VALIDATED,
23
24
  CUSTOM_VALIDATED_NOSQL_INJECTION,
24
25
  LIMITED_CHARS,
25
26
  STRING_TYPE_CHECKED,
26
27
  },
27
- Rule,
28
+ Rule: { NOSQL_INJECTION_MONGO },
28
29
  isNonEmptyObject,
29
- traverseValues
30
+ traverseValues,
31
+ isString
30
32
  } = require('@contrast/common');
31
33
  const utils = require('../../tag-utils');
32
- const { patchType } = require('../common');
34
+ const { patchType, filterSafeTags } = require('../common');
33
35
 
34
36
  const collectionMethods = [
35
37
  'find',
@@ -46,9 +48,14 @@ const collectionMethods = [
46
48
  'deleteOne',
47
49
  'deleteMany',
48
50
  ];
51
+ const dbMethods = [
52
+ 'command',
53
+ 'eval'
54
+ ];
49
55
 
50
56
  const querySafeTags = [
51
57
  ALPHANUM_SPACE_HYPHEN,
58
+ CUSTOM_VALIDATED,
52
59
  CUSTOM_VALIDATED_NOSQL_INJECTION,
53
60
  LIMITED_CHARS,
54
61
  STRING_TYPE_CHECKED,
@@ -56,6 +63,7 @@ const querySafeTags = [
56
63
 
57
64
  module.exports = function(core) {
58
65
  const {
66
+ config,
59
67
  depHooks,
60
68
  logger,
61
69
  patcher,
@@ -63,7 +71,7 @@ module.exports = function(core) {
63
71
  assess: {
64
72
  dataflow: {
65
73
  tracker,
66
- sinks: { isVulnerable, reportFindings },
74
+ sinks: { isVulnerable, runInActiveSink, isLocked, reportFindings, reportSafePositive },
67
75
  eventFactory: { createSinkEvent }
68
76
  }
69
77
  }
@@ -74,18 +82,36 @@ module.exports = function(core) {
74
82
 
75
83
  instr.getVulnerabilityInfo = function getVulnerabilityInfo(query) {
76
84
  let vulnInfo = null;
85
+ let reportSafe = null;
86
+
87
+ if (isString(query)) {
88
+ const strInfo = tracker.getData(query);
89
+ if (strInfo) {
90
+ if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
91
+ vulnInfo = { strInfo };
92
+ } else {
93
+ reportSafe = { strInfo };
94
+ }
95
+ }
77
96
 
78
- if (!isNonEmptyObject(query)) return vulnInfo;
97
+ return { vulnInfo, reportSafe };
98
+ }
79
99
 
80
- traverseValues(query, (path, type, value) => {
100
+ if (!isNonEmptyObject(query)) return { vulnInfo, reportSafe };
101
+
102
+ traverseValues(query, (path, _type, value) => {
81
103
  const strInfo = tracker.getData(value);
82
- if (strInfo && isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
83
- vulnInfo = { path, strInfo };
84
- return true; // halts traversal
104
+ if (strInfo) {
105
+ if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
106
+ vulnInfo = { path, strInfo };
107
+ return true; // halts traversal
108
+ } else {
109
+ reportSafe = { path, strInfo };
110
+ }
85
111
  }
86
112
  });
87
113
 
88
- return vulnInfo;
114
+ return { vulnInfo, reportSafe };
89
115
  };
90
116
 
91
117
  instr.install = function() {
@@ -99,12 +125,10 @@ module.exports = function(core) {
99
125
 
100
126
  function patchCollection(mongodb, version) {
101
127
  for (const method of collectionMethods) {
102
-
103
128
  const proto = mongodb.Collection.prototype;
104
129
  const name = `mongodb.Collection.prototype.${method}`;
105
130
 
106
131
  if (!proto[method]) {
107
-
108
132
  logger.trace({ name, version }, 'method not found - skipping instrumentation');
109
133
  continue;
110
134
  }
@@ -113,49 +137,61 @@ module.exports = function(core) {
113
137
  name,
114
138
  patchType,
115
139
  around(next, data) {
116
- const { obj, args } = data;
140
+ const { obj, args: origArgs } = data;
117
141
  const sourceCtx = sources.getStore()?.assess;
118
142
 
119
- if (instrumentation.isLocked() || !sourceCtx) {
143
+ if (isLocked(NOSQL_INJECTION_MONGO) || instrumentation.isLocked() || !sourceCtx) {
120
144
  return next();
121
145
  }
122
146
 
123
147
  const argIdx = 0;
124
148
  try {
125
- const vulnInfo = instr.getVulnerabilityInfo(args[argIdx]);
126
- if (vulnInfo) {
127
- const { path, strInfo } = vulnInfo;
128
- const objName = getObjectName(obj);
129
- const args = data.args.map((arg, idx) => ({
130
- value: inspect(arg),
131
- tracked: idx === argIdx,
132
- }));
133
-
134
- const tags = getAdjustedQueryTags(path, strInfo, args[argIdx].value);
135
- const resultVal = args[args.length - 1].value.startsWith('[Function') ? '' : 'Promise';
136
- const sinkEvent = createSinkEvent({
137
- args,
138
- context: `${objName}.${method}(${args.map((a) => a.value)})`,
139
- history: [strInfo],
140
- object: {
141
- tracked: false,
142
- value: 'mongodb.Collection',
143
- },
149
+ const { vulnInfo, reportSafe } = instr.getVulnerabilityInfo(origArgs[argIdx]);
150
+
151
+ if (!vulnInfo) {
152
+ reportSafe && config.assess.safe_positives.enable && reportSafePositive({
144
153
  name,
145
- result: {
146
- tracked: false,
147
- value: resultVal,
154
+ ruleId: NOSQL_INJECTION_MONGO,
155
+ safeTags: filterSafeTags(querySafeTags, reportSafe.strInfo),
156
+ strInfo: {
157
+ value: inspect(origArgs[argIdx]),
158
+ tags: getAdjustedQueryTags(reportSafe.path, reportSafe.strInfo, inspect(origArgs[argIdx])),
148
159
  },
149
- source: `P${argIdx}`,
150
- stacktraceOpts: {
151
- constructorOpt: data.hooked,
152
- },
153
- tags,
154
160
  });
161
+ return method === 'findOne' ? runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next()) : next();
162
+ }
155
163
 
156
- if (sinkEvent) {
157
- reportFindings({ ruleId: Rule.NOSQL_INJECTION_MONGO, sinkEvent });
158
- }
164
+ const { path, strInfo } = vulnInfo;
165
+ const objName = getObjectName(obj);
166
+ const args = origArgs.map((arg, idx) => ({
167
+ value: inspect(arg),
168
+ tracked: idx === argIdx,
169
+ }));
170
+
171
+ const tags = getAdjustedQueryTags(path, strInfo, args[argIdx].value);
172
+ const resultVal = args[args.length - 1].value.startsWith('[Function') ? '' : 'Promise';
173
+ const sinkEvent = createSinkEvent({
174
+ args,
175
+ context: `${objName}.${method}(${args.map((a) => a.value)})`,
176
+ history: [strInfo],
177
+ object: {
178
+ tracked: false,
179
+ value: 'mongodb.Collection',
180
+ },
181
+ name,
182
+ result: {
183
+ tracked: false,
184
+ value: resultVal,
185
+ },
186
+ source: `P${argIdx}`,
187
+ stacktraceOpts: {
188
+ constructorOpt: data.hooked,
189
+ },
190
+ tags,
191
+ });
192
+
193
+ if (sinkEvent) {
194
+ reportFindings({ ruleId: NOSQL_INJECTION_MONGO, sinkEvent });
159
195
  }
160
196
  } catch (err) {
161
197
  core.logger.error({ name, err }, 'assess sink analysis failed');
@@ -163,11 +199,7 @@ module.exports = function(core) {
163
199
 
164
200
  if (method === 'findOne') {
165
201
  // `findOne` will call `find` so don't analyze in nested call
166
- const store = { name, lock: true };
167
- const ret = instrumentation.run(store, next);
168
- // but unlock for when callback args run or returned Promises resolve/reject
169
- store.lock = false;
170
- return ret;
202
+ return runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next());
171
203
  }
172
204
 
173
205
  return next();
@@ -176,16 +208,101 @@ module.exports = function(core) {
176
208
  }
177
209
  }
178
210
 
179
-
180
211
  function patchDatabase(mongodb, version) {
181
- // todo
212
+ for (const method of dbMethods) {
213
+ const proto = mongodb.Db.prototype;
214
+ const name = `mongodb.Db.prototype.${method}`;
215
+
216
+ if (!proto[method]) {
217
+ logger.trace({ name, version }, 'method not found - skipping instrumentation');
218
+ continue;
219
+ }
220
+
221
+ patcher.patch(proto, method, {
222
+ name,
223
+ patchType,
224
+ around(next, data) {
225
+ const { obj, args: origArgs } = data;
226
+ const sourceCtx = sources.getStore()?.assess;
227
+
228
+ if (isLocked(NOSQL_INJECTION_MONGO) || instrumentation.isLocked() || !sourceCtx) {
229
+ return next();
230
+ }
231
+
232
+ const argIdx = 0;
233
+ try {
234
+ const { vulnInfo, reportSafe } = instr.getVulnerabilityInfo(origArgs[argIdx]);
235
+
236
+ if (!vulnInfo) {
237
+ const tags = reportSafe?.path ? getAdjustedQueryTags(reportSafe.path, reportSafe.strInfo, inspect(origArgs[argIdx])) : reportSafe?.strInfo?.tags;
238
+ reportSafe && config.assess.safe_positives.enable && reportSafePositive({
239
+ name,
240
+ ruleId: NOSQL_INJECTION_MONGO,
241
+ safeTags: filterSafeTags(querySafeTags, reportSafe.strInfo),
242
+ strInfo: {
243
+ value: inspect(origArgs[argIdx]),
244
+ tags
245
+ },
246
+ });
247
+ return method === 'eval' ? runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next()) : next();
248
+ }
249
+
250
+ const { path, strInfo } = vulnInfo;
251
+ const objName = getObjectName(obj, 'Db');
252
+ const args = origArgs.map((arg, idx) => ({
253
+ value: isString(arg) ? arg : inspect(arg),
254
+ tracked: idx === argIdx,
255
+ }));
256
+
257
+ const tags = path ? getAdjustedQueryTags(path, strInfo, args[argIdx].value) : strInfo?.tags;
258
+ const resultVal = args[args.length - 1].value.startsWith('[Function') ? '' : 'Promise';
259
+ const sinkEvent = createSinkEvent({
260
+ args,
261
+ context: `${objName}.${method}(${args.map((a) => a.value)})`,
262
+ history: [strInfo],
263
+ object: {
264
+ tracked: false,
265
+ value: 'mongodb.Db',
266
+ },
267
+ name,
268
+ result: {
269
+ tracked: false,
270
+ value: resultVal,
271
+ },
272
+ source: `P${argIdx}`,
273
+ stacktraceOpts: {
274
+ constructorOpt: data.hooked,
275
+ },
276
+ tags,
277
+ });
278
+
279
+ if (sinkEvent) {
280
+ reportFindings({ ruleId: NOSQL_INJECTION_MONGO, sinkEvent });
281
+ }
282
+ } catch (err) {
283
+ core.logger.error({ name, err }, 'assess sink analysis failed');
284
+ }
285
+
286
+ if (method === 'eval') {
287
+ // `eval` will call `command` so don't analyze in nested call
288
+ return runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next());
289
+ }
290
+
291
+ return next();
292
+ },
293
+ });
294
+ }
182
295
  }
183
296
 
184
- function getObjectName(obj) {
297
+ function getObjectName(obj, entity) {
185
298
  let name = '';
186
299
  name += obj.s?.namespace?.db || 'db';
187
- name += '.';
188
- name += obj.s?.namespace?.collection || 'collection';
300
+
301
+ if (entity !== 'Db') {
302
+ name += '.';
303
+ name += obj.s?.namespace?.collection || 'collection';
304
+ }
305
+
189
306
  return name;
190
307
  }
191
308
 
@@ -17,7 +17,7 @@
17
17
 
18
18
  const {
19
19
  DataflowTag: { UNTRUSTED, SQL_ENCODED, LIMITED_CHARS, CUSTOM_VALIDATED, CUSTOM_ENCODED },
20
- Rule,
20
+ Rule: { SQL_INJECTION },
21
21
  isString
22
22
  } = require('@contrast/common');
23
23
  const { createModuleLabel } = require('../../propagation/common');
@@ -38,7 +38,7 @@ module.exports = function (core) {
38
38
  assess: {
39
39
  dataflow: {
40
40
  tracker,
41
- sinks: { isVulnerable, reportFindings },
41
+ sinks: { isVulnerable, isLocked, reportFindings },
42
42
  eventFactory: { createSinkEvent },
43
43
  },
44
44
  },
@@ -46,7 +46,12 @@ module.exports = function (core) {
46
46
 
47
47
  const pre = (name, obj, version) => (data) => {
48
48
  const store = sources.getStore()?.assess;
49
- if (!store || !data.args[0] || !isString(data.args[0])) return;
49
+ if (
50
+ !store ||
51
+ !data.args[0] ||
52
+ !isString(data.args[0]) ||
53
+ isLocked(SQL_INJECTION)
54
+ ) return;
50
55
 
51
56
  const strInfo = tracker.getData(data.args[0]);
52
57
  if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
@@ -75,7 +80,7 @@ module.exports = function (core) {
75
80
 
76
81
  if (event) {
77
82
  reportFindings({
78
- ruleId: Rule.SQL_INJECTION,
83
+ ruleId: SQL_INJECTION,
79
84
  sinkEvent: event,
80
85
  });
81
86
  }
@@ -16,7 +16,19 @@
16
16
  'use strict';
17
17
 
18
18
  const { patchType } = require('../common');
19
- const { Rule, isString, DataflowTag: { CUSTOM_ENCODED_SQL_INJECTION, CUSTOM_ENCODED, CUSTOM_VALIDATED_SQL_INJECTION, CUSTOM_VALIDATED, SQL_ENCODED, LIMITED_CHARS, UNTRUSTED } } = require('@contrast/common');
19
+ const {
20
+ Rule: { SQL_INJECTION },
21
+ isString,
22
+ DataflowTag: {
23
+ CUSTOM_ENCODED_SQL_INJECTION,
24
+ CUSTOM_ENCODED,
25
+ CUSTOM_VALIDATED_SQL_INJECTION,
26
+ CUSTOM_VALIDATED,
27
+ SQL_ENCODED,
28
+ LIMITED_CHARS,
29
+ UNTRUSTED
30
+ },
31
+ } = require('@contrast/common');
20
32
 
21
33
  const safeTags = [
22
34
  CUSTOM_ENCODED_SQL_INJECTION,
@@ -35,7 +47,7 @@ module.exports = function (core) {
35
47
  assess: {
36
48
  dataflow: {
37
49
  tracker,
38
- sinks: { isVulnerable, reportFindings },
50
+ sinks: { isVulnerable, isLocked, reportFindings },
39
51
  eventFactory: { createSinkEvent },
40
52
  },
41
53
  },
@@ -53,7 +65,11 @@ module.exports = function (core) {
53
65
 
54
66
  const pre = (module, file, obj) => (data) => {
55
67
  const store = sources.getStore()?.assess;
56
- if (!store || !data.args[0]) return;
68
+ if (
69
+ !store ||
70
+ !data.args[0] ||
71
+ isLocked(SQL_INJECTION)
72
+ ) return;
57
73
 
58
74
  const val = getValueFromArgs(data.args);
59
75
  if (!val) return;
@@ -85,7 +101,7 @@ module.exports = function (core) {
85
101
 
86
102
  if (event) {
87
103
  reportFindings({
88
- ruleId: Rule.SQL_INJECTION,
104
+ ruleId: SQL_INJECTION,
89
105
  sinkEvent: event,
90
106
  });
91
107
  }
@@ -18,20 +18,21 @@
18
18
  const util = require('util');
19
19
  const {
20
20
  DataflowTag: { UNTRUSTED, SQL_ENCODED, LIMITED_CHARS, CUSTOM_VALIDATED, CUSTOM_ENCODED },
21
- Rule,
21
+ Rule: { SQL_INJECTION: ruleId },
22
22
  isString
23
23
  } = require('@contrast/common');
24
- const { patchType } = require('../common');
24
+ const { filterSafeTags, patchType } = require('../common');
25
25
 
26
26
  module.exports = function (core) {
27
27
  const {
28
+ config,
28
29
  depHooks,
29
30
  patcher,
30
31
  scopes: { sources },
31
32
  assess: {
32
33
  dataflow: {
33
34
  tracker,
34
- sinks: { isVulnerable, reportFindings },
35
+ sinks: { isVulnerable, isLocked, reportFindings, reportSafePositive },
35
36
  eventFactory: { createSinkEvent },
36
37
  },
37
38
  },
@@ -48,15 +49,17 @@ module.exports = function (core) {
48
49
 
49
50
  const postgres = core.assess.dataflow.sinks.postgres = {};
50
51
 
51
- const preHook = (methodSignature, version, mod, obj) => (data) => {
52
+ const preHook = (methodSignature) => (data) => {
52
53
  const assessStore = sources.getStore()?.assess;
53
- if (!assessStore) return;
54
+ if (!assessStore || isLocked(ruleId)) return;
54
55
 
55
56
  const [arg0] = data.args;
56
57
  const query = arg0?.text || arg0;
57
58
  if (!query || !isString(query)) return;
58
59
 
59
60
  const strInfo = tracker.getData(query);
61
+ // todo: if the query isn't tracked but users are sending tracked strings in the values
62
+ // array (args[1]), then shouldn't we report a "safe-positive" event of some kind?
60
63
  if (!strInfo) return;
61
64
 
62
65
  const objValue = methodSignature.includes('native') ? 'pg.native.Client' : 'pg.Client';
@@ -88,10 +91,20 @@ module.exports = function (core) {
88
91
 
89
92
  if (event) {
90
93
  reportFindings({
91
- ruleId: Rule.SQL_INJECTION,
94
+ ruleId,
92
95
  sinkEvent: event,
93
96
  });
94
97
  }
98
+ } else if (config.assess.safe_positives.enable) {
99
+ reportSafePositive({
100
+ name: methodSignature,
101
+ ruleId,
102
+ safeTags: filterSafeTags(safeTags, strInfo),
103
+ strInfo: {
104
+ value: strInfo.value,
105
+ tags: strInfo.tags,
106
+ }
107
+ });
95
108
  }
96
109
  };
97
110
 
@@ -99,11 +112,11 @@ module.exports = function (core) {
99
112
  const pgClientQueryPatchName = 'pg.Client.prototype.query';
100
113
  depHooks.resolve(
101
114
  { name: 'pg', file: 'lib/client.js' },
102
- (client, version) => {
115
+ (client) => {
103
116
  patcher.patch(client.prototype, 'query', {
104
117
  name: pgClientQueryPatchName,
105
118
  patchType,
106
- pre: preHook('pg/lib/client.prototype.query', version, 'pg', 'Client'),
119
+ pre: preHook('pg/lib/client.prototype.query'),
107
120
  });
108
121
  },
109
122
  );
@@ -111,16 +124,16 @@ module.exports = function (core) {
111
124
  const pgNativeClientQueryPatchName = 'pg.native.Client.prototype.query';
112
125
  depHooks.resolve(
113
126
  { name: 'pg', file: 'lib/native/client.js' },
114
- (client, version) => {
127
+ (client) => {
115
128
  patcher.patch(client.prototype, 'query', {
116
129
  name: pgNativeClientQueryPatchName,
117
130
  patchType,
118
- pre: preHook('pg/lib/native/client.prototype.query', version, 'pg', 'native.Client'),
131
+ pre: preHook('pg/lib/native/client.prototype.query'),
119
132
  });
120
133
  },
121
134
  );
122
135
 
123
- depHooks.resolve({ name: 'pg-pool' }, (pool, version) => {
136
+ depHooks.resolve({ name: 'pg-pool' }, (pool) => {
124
137
  const name = 'pg-pool.Pool.prototype.query';
125
138
  patcher.patch(pool.prototype, 'query', {
126
139
  name,
@@ -137,7 +150,7 @@ module.exports = function (core) {
137
150
  return;
138
151
  }
139
152
 
140
- preHook(name, version, 'pg-pool', 'Pool')(data);
153
+ preHook(name)(data);
141
154
  },
142
155
  });
143
156
  });
@@ -0,0 +1,142 @@
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
+ Rule: { SQL_INJECTION },
21
+ DataflowTag: {
22
+ UNTRUSTED,
23
+ SQL_ENCODED,
24
+ LIMITED_CHARS,
25
+ CUSTOM_VALIDATED,
26
+ CUSTOM_ENCODED,
27
+ },
28
+ } = require('@contrast/common');
29
+ const { patchType, filterSafeTags } = require('../common');
30
+
31
+ module.exports = function (core) {
32
+ const {
33
+ depHooks,
34
+ patcher,
35
+ config,
36
+ scopes: { sources },
37
+ assess: {
38
+ dataflow: {
39
+ tracker,
40
+ sinks: { isVulnerable, runInActiveSink, reportFindings, reportSafePositive },
41
+ eventFactory: { createSinkEvent },
42
+ },
43
+ },
44
+ } = core;
45
+
46
+ const safeTags = [
47
+ SQL_ENCODED,
48
+ LIMITED_CHARS,
49
+ CUSTOM_VALIDATED,
50
+ CUSTOM_ENCODED
51
+ ];
52
+ const requiredTag = UNTRUSTED;
53
+ const inspect = patcher.unwrap(util.inspect);
54
+
55
+ const sequelize = (core.assess.dataflow.sinks.sequelize = {});
56
+
57
+ sequelize.install = function () {
58
+ const sequelizeQueryPatchName = 'sequelize.prototype.query';
59
+ depHooks.resolve({ name: 'sequelize' }, (sequelize) => {
60
+ patcher.patch(sequelize.prototype, 'query', {
61
+ name: sequelizeQueryPatchName,
62
+ patchType,
63
+ around(next, data) {
64
+ const { args, hooked, orig } = data;
65
+ const sourceContext = sources.getStore()?.assess;
66
+ if (!sourceContext || !args[0]) return next();
67
+
68
+ const query = typeof args[0] === 'string' ? args[0] : args[0].query;
69
+
70
+ try {
71
+ const queryInfo = tracker.getData(query);
72
+ const isVulnerableQuery = isVulnerable(requiredTag, safeTags, queryInfo.tags);
73
+
74
+ if (queryInfo && !isVulnerableQuery && config.assess.safe_positives.enable) {
75
+ reportSafePositive({
76
+ name: sequelizeQueryPatchName,
77
+ ruleId: SQL_INJECTION,
78
+ safeTags: filterSafeTags(safeTags, queryInfo),
79
+ strInfo: {
80
+ value: queryInfo?.value,
81
+ tags: queryInfo.tags,
82
+ },
83
+ });
84
+ }
85
+
86
+ if (
87
+ !queryInfo ||
88
+ !isVulnerableQuery
89
+ ) {
90
+ return runInActiveSink(SQL_INJECTION, async () => await next());
91
+ }
92
+
93
+ const sqlValue =
94
+ typeof args[0] === 'string' ? args[0] : inspect(args[0]);
95
+ const inspectedOptions = args[1] ? inspect(args[1]) : '';
96
+ const contextArgs = args[1]
97
+ ? `${sqlValue}, ${inspectedOptions}`
98
+ : sqlValue;
99
+
100
+ const reportedArgs = [{ value: sqlValue, tracked: true }];
101
+ args[1] &&
102
+ reportedArgs.push({ value: inspectedOptions, tracked: false });
103
+
104
+ const event = createSinkEvent({
105
+ context: `sequelize.prototype.query(${contextArgs})`,
106
+ name: sequelizeQueryPatchName,
107
+ history: [queryInfo],
108
+ object: {
109
+ value: 'sequelize.prototype',
110
+ tracked: false,
111
+ },
112
+ args: reportedArgs,
113
+ tags: queryInfo?.tags,
114
+ source: 'P0',
115
+ stacktraceOpts: {
116
+ contructorOpt: hooked,
117
+ prependFrames: [orig],
118
+ },
119
+ });
120
+
121
+ if (event) {
122
+ reportFindings({
123
+ ruleId: SQL_INJECTION,
124
+ sinkEvent: event,
125
+ });
126
+ }
127
+ /* c8 ignore next 3 */
128
+ } catch (err) {
129
+ core.logger.error(
130
+ { name: sequelizeQueryPatchName, err },
131
+ 'assess sink analysis failed'
132
+ );
133
+ }
134
+
135
+ return runInActiveSink(SQL_INJECTION, async () => await next());
136
+ },
137
+ });
138
+ });
139
+ };
140
+
141
+ return sequelize;
142
+ };