@contrast/assess 1.70.1 → 1.71.1

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.
@@ -22,6 +22,7 @@ module.exports = function(core) {
22
22
 
23
23
  require('./install/contrast-methods')(core);
24
24
  require('./install/ejs')(core);
25
+ core.initComponentSync(require('./install/jsonwebtoken'));
25
26
  require('./install/mongoose')(core);
26
27
  require('./install/pug')(core);
27
28
  require('./install/sequelize')(core);
@@ -0,0 +1,333 @@
1
+ /*
2
+
3
+ * Copyright: 2026 Contrast Security, Inc
4
+ * Contact: support@contrastsecurity.com
5
+ * License: Commercial
6
+
7
+ * NOTICE: This Software and the patented inventions embodied within may only be
8
+ * used as part of Contrast Security’s commercial offerings. Even though it is
9
+ * made available through public repositories, use of this Software is subject to
10
+ * the applicable End User Licensing Agreement found at
11
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
12
+ * between Contrast Security and the End User. The Software may not be reverse
13
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
14
+ * way not consistent with the End User License Agreement.
15
+ */
16
+ 'use strict';
17
+
18
+ const { kComponentName, ComponentBase } = require('@contrast/core/lib/ioc/core');
19
+ const { isString, set, traverseValues, primordials } = require('@contrast/common');
20
+ const { createFullLengthCopyTags } = require('../../tag-utils');
21
+ const { patchType } = require('../common');
22
+
23
+ const COMPONENT_NAME = 'assess.dataflow.propagation.jsonwebtoken';
24
+ const OBJECT_DTM = Object.freeze({ value: 'jsonwebtoken', tracked: false });
25
+ const MODULE_NAME = 'jsonwebtoken';
26
+ const METHODS = {
27
+ DECODE: 'decode',
28
+ VERIFY: 'verify',
29
+ SIGN: 'sign',
30
+ };
31
+ const SOURCE = 'A';
32
+ const TARGET = 'R';
33
+
34
+ module.exports = class Instrumentation extends ComponentBase {
35
+
36
+ static [kComponentName] = COMPONENT_NAME;
37
+
38
+ /** calls around hook's next callback in no-instrumentation scope */
39
+ _execNext(next) {
40
+ return this.core.scopes.instrumentation.isLocked() ?
41
+ next() :
42
+ this.core.scopes.instrumentation.run({ lock: true, name: COMPONENT_NAME }, next);
43
+ }
44
+
45
+ /**
46
+ * The expected API usage for all methods puts callbacks at position 2 or 3.
47
+ * jwt.sign(payload, secretOrPrivateKey, [options, callback])
48
+ * jwt.verify(token, secretOrPublicKey, [options, callback])
49
+ */
50
+ _getCallbackIdx(args) {
51
+ if (typeof args[2] == 'function') return 2;
52
+ if (typeof args[3] == 'function') return 3;
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * Traverses parsed object to track values with tags from token input.
58
+ * Used by decode and verify instrumentation.
59
+ */
60
+ _traverseAndTrack(target, argStrInfo, data, methodName) {
61
+ const self = this;
62
+ const {
63
+ assess: { dataflow: { tracker } }
64
+ } = self.core;
65
+
66
+ traverseValues(target, (path, type, value) => {
67
+ if (isString(value) && value.length) {
68
+
69
+ const p = primordials.ArrayPrototypeJoin.call(path, '.');
70
+ const pEvent = self._makeEvent({
71
+ methodName,
72
+ result: { tracked: true, value },
73
+ args: [{ value: argStrInfo.value, tracked: true }],
74
+ tags: createFullLengthCopyTags(argStrInfo.tags, value.length),
75
+ history: [argStrInfo],
76
+ stacktraceOpts: { constructorOpt: data.hooked, },
77
+ });
78
+ if (pEvent) {
79
+ const resultStrInfo = tracker.track(value, pEvent);
80
+ if (resultStrInfo) {
81
+ set(target, p, resultStrInfo.extern);
82
+ }
83
+ }
84
+ }
85
+ });
86
+ }
87
+
88
+ _makeEvent(obj) {
89
+ obj.object = OBJECT_DTM;
90
+ obj.name = COMPONENT_NAME;
91
+ obj.moduleName = MODULE_NAME;
92
+ obj.source = SOURCE;
93
+ obj.target = TARGET;
94
+ return this.core.assess.eventFactory.createPropagationEvent(obj);
95
+ }
96
+
97
+ install() {
98
+ this.core.depHooks.resolve({ name: MODULE_NAME, version: '9', }, (xports, meta) => {
99
+ this.patchDecode(xports);
100
+ this.patchSign(xports);
101
+ this.patchVerify(xports);
102
+ });
103
+ }
104
+
105
+ patchDecode(xports) {
106
+ const self = this;
107
+ const {
108
+ assess: { getPropagatorContext, dataflow: { tracker } },
109
+ patcher,
110
+ } = this.core;
111
+
112
+ patcher.patch(xports, METHODS.DECODE, {
113
+ name: `${MODULE_NAME}.${METHODS.DECODE}`,
114
+ patchType,
115
+ around(next, data) {
116
+ const ctx = getPropagatorContext();
117
+ const strInfo = tracker.getData(data.args[0]);
118
+
119
+ let ret = self._execNext(next);
120
+
121
+ if (ctx && strInfo) {
122
+ if (typeof ret == 'object') {
123
+ self._traverseAndTrack(ret, strInfo, data, METHODS.DECODE);
124
+ } else if (isString(ret)) {
125
+ const event = self._makeEvent({
126
+ methodName: METHODS.DECODE,
127
+ result: { tracked: true, value: ret },
128
+ args: [{ value: strInfo.value, tracked: true }],
129
+ tags: createFullLengthCopyTags(strInfo.tags, ret.length),
130
+ history: [strInfo],
131
+ stacktraceOpts: { constructorOpt: data.hooked, },
132
+ });
133
+ if (event) {
134
+ const resultStrInfo = tracker.track(ret, event);
135
+ if (resultStrInfo) {
136
+ ret = resultStrInfo.extern;
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ return ret;
143
+ },
144
+ });
145
+ }
146
+
147
+ patchSign(xports) {
148
+ const self = this;
149
+ const {
150
+ assess: { getPropagatorContext, dataflow: { tracker } },
151
+ patcher,
152
+ scopes,
153
+ } = this.core;
154
+
155
+ const ARGS_DTM = [{ value: '[Obejct]', tracked: true }];
156
+
157
+ function _collectTagNames(set, strInfo) {
158
+ if (strInfo?.tags)
159
+ for (const t of Object.keys(strInfo.tags))
160
+ set.add(t);
161
+ }
162
+
163
+ function _makeTagsObj(tagSet, len) {
164
+ return Array.from(tagSet).reduce((acc, t) => {
165
+ acc[t] = [0, len];
166
+ return acc;
167
+ }, Object.create(null));
168
+ }
169
+
170
+ patcher.patch(xports, METHODS.SIGN, {
171
+ name: `${MODULE_NAME}.${METHODS.SIGN}`,
172
+ patchType,
173
+ around(next, data) {
174
+ const ctx = getPropagatorContext();
175
+ const callbackIdx = self._getCallbackIdx(data.args);
176
+ let tagSet;
177
+ let historySet;
178
+
179
+ // collect all tags from all of the payload object (or string) values
180
+ // check if we're in request scope
181
+ if (ctx && typeof data.args[0] == 'object') {
182
+ tagSet = new Set();
183
+ historySet = new Set();
184
+ traverseValues(data.args[0], (path, type, value) => {
185
+ const info = tracker.getData(value);
186
+ if (info?.tags) {
187
+ historySet.add(info);
188
+ for (const tag of Object.keys(info.tags)) {
189
+ tagSet.add(tag);
190
+ }
191
+ }
192
+ });
193
+ } else if (ctx && isString(data.args[0])) {
194
+ tagSet = new Set();
195
+ const info = tracker.getData(data.args[0]);
196
+ _collectTagNames(set, info);
197
+ }
198
+
199
+ if (callbackIdx) {
200
+ // always run in no-instrumentation even if we're not in request scope
201
+ const o = data.args[callbackIdx];
202
+ data.args[callbackIdx] = function () {
203
+ if (arguments[1] && tagSet) {
204
+ // track result if if parsing was successful and there are tags on payload
205
+ const event = self._makeEvent({
206
+ methodName: METHODS.SIGN,
207
+ result: { tracked: true, value: arguments[1] },
208
+ args: ARGS_DTM,
209
+ tags: _makeTagsObj(tagSet, arguments[1].length - 1),
210
+ history: Array.from(historySet),
211
+ stacktraceOpts: { constructorOpt: o, },
212
+ });
213
+ if (event) {
214
+ const strInfo = tracker.track(arguments[1], event);
215
+ if (strInfo) {
216
+ arguments[1] = strInfo.extern;
217
+ }
218
+ }
219
+ }
220
+ // ensure instrumentation resumes in callback
221
+ const store = scopes.instrumentation.getStore();
222
+ if (store?.lock && store?.name == COMPONENT_NAME) store.lock = false;
223
+ return scopes.instrumentation.run(store, o, ...arguments);
224
+ };
225
+ }
226
+
227
+ let ret = self._execNext(next);
228
+
229
+ if (tagSet && !callbackIdx) {
230
+ // track result if if parsing was successful and there are tags on payload
231
+ const event = self._makeEvent({
232
+ methodName: METHODS.SIGN,
233
+ result: { tracked: true, value: ret },
234
+ args: ARGS_DTM,
235
+ tags: _makeTagsObj(tagSet, ret.length - 1),
236
+ history: Array.from(historySet),
237
+ stacktraceOpts: { constructorOpt: data.hooked, },
238
+ });
239
+
240
+ if (event) {
241
+ const strInfo = tracker.track(ret, event);
242
+ if (strInfo) {
243
+ ret = strInfo.extern;
244
+ }
245
+ }
246
+ }
247
+
248
+ return ret;
249
+ },
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Turns off instrumentation within jsonwebtoken.verify(token, secret[, callback]),
255
+ * but ensures that any values in the parsed token results are tracked accordingly.
256
+ */
257
+ patchVerify(xports) {
258
+ const self = this;
259
+ const {
260
+ assess: { getPropagatorContext, dataflow: { tracker } },
261
+ patcher,
262
+ scopes,
263
+ } = this.core;
264
+
265
+ patcher.patch(xports, METHODS.VERIFY, {
266
+ patchType,
267
+ name: `${MODULE_NAME}.${METHODS.VERIFY}`,
268
+ around(next, data) {
269
+ const { args } = data;
270
+ const ctx = getPropagatorContext();
271
+ const strInfo = tracker.getData(args[0]); // args[0] = token;
272
+ const callbackIdx = self._getCallbackIdx(args);
273
+
274
+ if (callbackIdx) {
275
+ // always run in no-instrumentation even if we're not in request scope
276
+ const o = args[callbackIdx];
277
+ args[callbackIdx] = function() {
278
+ if (ctx) {
279
+ if (typeof arguments[1] == 'object') {
280
+ self._traverseAndTrack(arguments[1], strInfo, data, METHODS.VERIFY);
281
+ } else if (isString(arguments[1])) {
282
+ const event = self._makeEvent({
283
+ methodName: METHODS.VERIFY,
284
+ result: { tracked: true, value: arguments[1] },
285
+ args: [{ value: strInfo.value, tracked: true }],
286
+ tags: createFullLengthCopyTags(strInfo.tags, arguments[1].length),
287
+ history: [strInfo],
288
+ stacktraceOpts: { constructorOpt: data.hooked, },
289
+ });
290
+ if (event) {
291
+ const resultStrInfo = tracker.track(arguments[1], event);
292
+ if (resultStrInfo) {
293
+ arguments[1] = resultStrInfo.extern;
294
+ }
295
+ }
296
+ }
297
+ }
298
+ // ensure instrumentation resumes in callback
299
+ const store = scopes.instrumentation.getStore();
300
+ if (store?.lock && store?.name == COMPONENT_NAME) store.lock = false;
301
+ return scopes.instrumentation.run(store, o, ...arguments);
302
+ };
303
+ }
304
+
305
+ let ret = self._execNext(next);
306
+
307
+ // if no callback was used, track here in "post" hook
308
+ if (ctx && callbackIdx == null) {
309
+ if (typeof ret == 'object') {
310
+ self._traverseAndTrack(ret, strInfo, data, METHODS.VERIFY);
311
+ } else if (isString(ret)) {
312
+ const event = self._makeEvent({
313
+ methodName: METHODS.VERIFY,
314
+ result: { tracked: true, value: ret },
315
+ args: [{ value: strInfo.value, tracked: true }],
316
+ tags: createFullLengthCopyTags(strInfo.tags, ret.length),
317
+ history: [strInfo],
318
+ stacktraceOpts: { constructorOpt: data.hooked, },
319
+ });
320
+ if (event) {
321
+ const resultStrInfo = tracker.track(ret, event);
322
+ if (resultStrInfo) {
323
+ ret = resultStrInfo.extern;
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ return ret;
330
+ },
331
+ });
332
+ }
333
+ };
@@ -373,7 +373,13 @@ module.exports = function (core) {
373
373
  }
374
374
 
375
375
  instr.install = function () {
376
- depHooks.resolve({ name: 'mongodb', version: '<7' }, (mongodb, version) => {
376
+ depHooks.resolve({ name: 'mongodb', version: '<7' }, (mongodb, meta) => {
377
+ if (!mongodb.Collection || !mongodb.Db) {
378
+ meta.rerun();
379
+ return;
380
+ }
381
+
382
+ const { version } = meta;
377
383
  patchCollection(mongodb, version);
378
384
  patchDatabase(mongodb, version);
379
385
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/assess",
3
- "version": "1.70.1",
3
+ "version": "1.71.1",
4
4
  "description": "Contrast service providing framework-agnostic Assess support",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -21,18 +21,18 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@contrast/common": "1.41.1",
24
- "@contrast/config": "1.58.1",
25
- "@contrast/core": "1.63.1",
26
- "@contrast/dep-hooks": "1.32.1",
24
+ "@contrast/config": "1.58.2",
25
+ "@contrast/core": "1.63.2",
26
+ "@contrast/dep-hooks": "1.32.2",
27
27
  "@contrast/distringuish": "6.0.2",
28
- "@contrast/instrumentation": "1.42.1",
29
- "@contrast/logger": "1.36.1",
30
- "@contrast/patcher": "1.35.1",
31
- "@contrast/rewriter": "1.40.1",
32
- "@contrast/route-coverage": "1.56.1",
33
- "@contrast/scopes": "1.33.1",
34
- "@contrast/sources": "1.9.1",
35
- "@contrast/stack-trace-factory": "1.3.1",
28
+ "@contrast/instrumentation": "1.42.2",
29
+ "@contrast/logger": "1.36.2",
30
+ "@contrast/patcher": "1.35.2",
31
+ "@contrast/rewriter": "1.40.2",
32
+ "@contrast/route-coverage": "1.56.2",
33
+ "@contrast/scopes": "1.33.2",
34
+ "@contrast/sources": "1.9.2",
35
+ "@contrast/stack-trace-factory": "1.3.2",
36
36
  "semver": "7.6.0"
37
37
  }
38
38
  }