@contrast/agent 4.24.2 → 4.25.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.
@@ -15,21 +15,22 @@ Copyright: 2022 Contrast Security, Inc
15
15
  'use strict';
16
16
 
17
17
  const agentEmitter = require('../../agent-emitter');
18
- const Helpers = require('../../core/express/utils');
18
+ const { EVENTS } = require('../../core/express/utils');
19
19
  const {
20
- prototype: { decorateRequest }
20
+ prototype: { decorateRequest },
21
21
  } = require('../../hooks/frameworks/base');
22
- const { EVENTS } = Helpers;
23
22
 
24
23
  class ExpressSources {
25
24
  constructor() {
26
25
  agentEmitter.on(EVENTS.REQUEST_READY, ExpressSources.handleReady);
27
26
  agentEmitter.on(EVENTS.BODY_PARSED, ExpressSources.handleBody);
28
27
  agentEmitter.on(EVENTS.COOKIES_PARSED, ExpressSources.handleCookies);
29
- agentEmitter.on(EVENTS.PARAM_PARSED, ExpressSources.handleParams);
28
+ agentEmitter.on(EVENTS.PARAMS_PARSED, ExpressSources.handleParams);
30
29
  }
31
30
 
32
31
  static handleReady(req, res, inputType) {
32
+ agentEmitter.emit('assess.url', req, '_parsedUrl');
33
+ req.originalUrl !== '/' && agentEmitter.emit('assess.url', req, 'originalUrl');
33
34
  agentEmitter.emit('assess.query', req, 'query');
34
35
  agentEmitter.emit('assess.headers', req, 'headers');
35
36
  }
@@ -133,7 +133,7 @@ module.exports = class SourceMembrane extends Membrane {
133
133
  * @return {string} returns tracked string with proper contrastProps
134
134
  */
135
135
  onString(str, metadata) {
136
- if (!this.ensureMetadata(metadata)) {
136
+ if (!this.ensureMetadata(metadata) && !metadata.rootLevelString) {
137
137
  return str;
138
138
  }
139
139
  const tracked = tracker.track(str);
@@ -18,9 +18,9 @@ const util = require('util');
18
18
  const CleanStack = require('../../util/clean-stack');
19
19
  const tracker = require('../../tracker');
20
20
  const stackFactory = require('../../core/stacktrace').singleton;
21
- const distringuish = require('@contrast/distringuish-prebuilt');
22
21
  const { PROXY_TARGET } = require('../../../lib/constants');
23
22
  const TagRange = require('../models/tag-range');
23
+ const { toUntrackedString } = require('../utils');
24
24
 
25
25
  /**
26
26
  * Holds information about the call context of a function
@@ -116,12 +116,13 @@ module.exports = class CallContext {
116
116
  }
117
117
  if (arg[key] && typeof arg[key] === 'object' && iteration < 100) {
118
118
  return CallContext.hasTrackedArg(arg[key], iteration += 1);
119
- }
119
+ }
120
120
  }
121
121
  }
122
122
  return false;
123
123
  }
124
124
 
125
+ // eslint-disable-next-line complexity
125
126
  static getDisplayRange(arg, orgArg = arg, iteration = 0) {
126
127
  if (tracker.getData(arg)) {
127
128
  return new TagRange(0, arg.length - 1, 'untrusted');
@@ -188,17 +189,7 @@ module.exports = class CallContext {
188
189
  */
189
190
  static valueString(value) {
190
191
  if (_.isString(value)) {
191
- // NODE-933 - we need to be careful not to assign
192
- // external strings to something that is referenced by its'
193
- // own properties. For source strings we were creating
194
- // a CallContext with the string, adding it to the SourceEvent,
195
- // and then that SourceEvent was added to the external strings
196
- // props. So there was a circular ref between the string
197
- // and it's props, preventing either from being freed.
198
- if (distringuish.isExternal(value.valueOf())) {
199
- return distringuish.internalize(value.valueOf());
200
- }
201
- return value.valueOf();
192
+ return toUntrackedString(value);
202
193
  }
203
194
 
204
195
  if (_.isNumber(value)) {
@@ -12,277 +12,236 @@ Copyright: 2022 Contrast Security, Inc
12
12
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
13
  way not consistent with the End User License Agreement.
14
14
  */
15
- /**
16
- * @module /lib/assess/sources
17
- */
18
-
19
15
  'use strict';
20
- const logger = require('../../core/logger')('contrast:assess:sources');
21
- const FormidableSource = require('./formidable');
16
+
17
+ const parseurl = require('parseurl');
22
18
  const agentEmitter = require('../../agent-emitter');
23
- const SourceMembrane = require('../membrane/source-membrane');
24
19
  const { AsyncStorage, KEYS } = require('../../core/async-storage');
25
- const parseurl = require('parseurl');
26
- const {
27
- EXCLUSION_INPUT_TYPES: { BODY, HEADER, PARAMETER, QUERYSTRING, COOKIE }
28
- } = require('../../constants');
20
+ const logger = require('../../core/logger')('contrast:assess:sources');
21
+ const { EXCLUSION_INPUT_TYPES } = require('../../constants');
22
+ const SourceMembrane = require('../membrane/source-membrane');
29
23
  const { Signature } = require('../models');
30
-
31
- const sources = module.exports;
32
-
33
24
  const { SourceEventHandler } = require('./event-handler');
25
+ const FormidableSource = require('./formidable');
34
26
 
35
27
  /**
36
- * Registers sources for assess instrumentation
37
- */
38
- sources.init = function(agent) {
39
- new FormidableSource(agent);
40
- };
41
-
42
- /**
43
- * @param {string} type name of source type (headers, params, etc)
44
- * @param {Object} parent object to watch property on, e.g., req
45
- * @param {string} key name of property (for req.query, 'query')
46
- * @param {Membrane} membrane used in testing to provide a custom membrane instance
28
+ * Shared logging for when URL/Input exclusions thwart handling of sources.
29
+ * @param {string} type the source type e.g. BODY, PARAMETER, HEADER
47
30
  */
48
- sources.track = function(type, parent, key, membrane) {
49
- const object = parent[key];
50
- if (!object) {
51
- return;
52
- }
53
- const metadata = Object.create(null);
54
- metadata.sourceType = type;
55
- parent[key] = membrane.wrap(object, metadata);
31
+ const logExclusionMessage = (type, exclusion) => {
32
+ const { name } = exclusion;
33
+ logger.debug(
34
+ 'excluding %s inputs from all assess rules (%s)',
35
+ type.toLowerCase(),
36
+ name
37
+ );
56
38
  };
57
39
 
58
- /**
59
- * Chooses a strategy for tracking the source events
60
- * @param {any} config Current configuration for the agent
61
- * @param {Logger} logger A logger instance
62
- * @returns {Boolean} whether lazy tracking is enabled or not
63
- */
64
- sources.getLazyTrackingConfig = function(config, logger) {
65
- if (config._default['agent.traverse_and_track']) {
66
- return config.assess.enable_lazy_tracking;
67
- }
68
- if (config._default['assess.enable_lazy_tracking']) {
69
- logger.error('agent.traverse_and_track option is deprecated. Please use assess.enable_lazy_tracking from now on. It\'s value should be the opposite of this one');
70
- return !config.agent.traverse_and_track;
71
- }
72
-
73
- logger.error('Conflicting options set: `agent.traverse_and_track` and `assess.enable_lazy_tracking`. `agent.traverse_and_track` is deprecated, so `assess.enable_lazy_tracking` takes precedence');
74
- return config.assess.enable_lazy_tracking;
75
- };
40
+ module.exports = {
41
+ /**
42
+ * Registers sources for assess instrumentation
43
+ */
44
+ init(agent) {
45
+ new FormidableSource(agent);
46
+ },
47
+
48
+ /**
49
+ * @param {string} type name of source type (headers, params, etc)
50
+ * @param {Object} parent object to watch property on, e.g., req
51
+ * @param {string} key name of property (for req.query, 'query')
52
+ * @param {Membrane} membrane used in testing to provide a custom membrane instance
53
+ */
54
+ track(type, parent, key, membrane) {
55
+ const object = parent[key];
56
+ if (!object) {
57
+ return;
58
+ }
59
+ const metadata = Object.create(null);
60
+ metadata.sourceType = type;
76
61
 
77
- /**
78
- * Registers an event to add URL and input exclusions to async storage if they
79
- * pertain to the current request path. Also registers all the source events
80
- * that are emitted in the framework instrumentation sources. This wraps an
81
- * object in a membrane
82
- */
83
- sources.registerListeners = function({ config, exclusions }) {
84
- const isLazyTrackingEnabled = sources.getLazyTrackingConfig(config, logger);
62
+ // We set this flag for string properties that are direct properties
63
+ // of the request object. Unlike parsed querystring values like
64
+ // { input: 'userInput' } these properties have an empty path, but still
65
+ // need to be tracked
66
+ metadata.rootLevelString = false;
85
67
 
86
- agentEmitter.on('assess.body', (obj, prop) => {
87
- if (isLazyTrackingEnabled) {
88
- return sources.handleSourceEvent(config, exclusions, BODY, obj, prop);
68
+ if (key === 'originalUrl') {
69
+ metadata.rootLevelString = true;
89
70
  }
90
71
 
91
- agentEmitter.emit('assess.source', {
92
- config,
93
- exclusions,
94
- obj,
95
- prop,
96
- data: obj[prop],
97
- type: BODY
98
- });
99
- });
100
- agentEmitter.on('assess.headers', (obj, prop) => {
101
- if (isLazyTrackingEnabled) {
102
- return sources.handleSourceEvent(config, exclusions, HEADER, obj, prop);
72
+ parent[key] = membrane.wrap(object, metadata);
73
+ },
74
+
75
+ /**
76
+ * Chooses a strategy for tracking the source events
77
+ * @param {any} config Current configuration for the agent
78
+ * @param {Logger} logger A logger instance
79
+ * @returns {Boolean} whether lazy tracking is enabled or not
80
+ */
81
+ getLazyTrackingConfig(config, logger) {
82
+ if (config._default['agent.traverse_and_track']) {
83
+ return config.assess.enable_lazy_tracking;
103
84
  }
104
85
 
105
- agentEmitter.emit('assess.source', {
106
- obj,
107
- prop,
108
- data: obj[prop],
109
- type: HEADER
110
- });
111
- });
112
- agentEmitter.on('assess.params', (obj, prop) => {
113
- if (isLazyTrackingEnabled) {
114
- return sources.handleSourceEvent(
115
- config,
116
- exclusions,
117
- PARAMETER,
118
- obj,
119
- prop
86
+ if (config._default['assess.enable_lazy_tracking']) {
87
+ logger.error(
88
+ "agent.traverse_and_track option is deprecated. Please use assess.enable_lazy_tracking from now on. It's value should be the opposite of this one"
120
89
  );
90
+ return !config.agent.traverse_and_track;
121
91
  }
122
92
 
123
- agentEmitter.emit('assess.source', {
124
- obj,
125
- prop,
126
- data: obj[prop],
127
- type: PARAMETER
128
- });
129
- });
130
- agentEmitter.on('assess.query', (obj, prop) => {
131
- if (isLazyTrackingEnabled) {
132
- return sources.handleSourceEvent(
93
+ logger.error(
94
+ 'Conflicting options set: `agent.traverse_and_track` and `assess.enable_lazy_tracking`. `agent.traverse_and_track` is deprecated, so `assess.enable_lazy_tracking` takes precedence'
95
+ );
96
+ return config.assess.enable_lazy_tracking;
97
+ },
98
+
99
+ /**
100
+ * Registers an event to add URL and input exclusions to async storage if they
101
+ * pertain to the current request path. Also registers all the source events
102
+ * that are emitted in the framework instrumentation sources. This wraps an
103
+ * object in a membrane
104
+ */
105
+ registerListeners({ config, exclusions }) {
106
+ const isLazyTrackingEnabled = this.getLazyTrackingConfig(config, logger);
107
+
108
+ const handleAssessEvent = (event, type) => {
109
+ agentEmitter.on(event, (obj, prop) => {
110
+ if (isLazyTrackingEnabled) {
111
+ return this.handleSourceEvent(config, exclusions, type, obj, prop);
112
+ }
113
+
114
+ const signature = new Signature({
115
+ moduleName: 'Object',
116
+ methodName: 'getter',
117
+ args: [prop],
118
+ return: 'String',
119
+ isModule: false,
120
+ });
121
+
122
+ new SourceEventHandler({ config, exclusions, signature, type }).handle({
123
+ obj,
124
+ prop,
125
+ });
126
+ });
127
+ };
128
+
129
+ handleAssessEvent('assess.url', EXCLUSION_INPUT_TYPES.PARAMETER);
130
+ handleAssessEvent('assess.body', EXCLUSION_INPUT_TYPES.BODY);
131
+ handleAssessEvent('assess.headers', EXCLUSION_INPUT_TYPES.HEADER);
132
+ handleAssessEvent('assess.params', EXCLUSION_INPUT_TYPES.PARAMETER);
133
+ handleAssessEvent('assess.query', EXCLUSION_INPUT_TYPES.QUERYSTRING);
134
+ handleAssessEvent('assess.cookies', EXCLUSION_INPUT_TYPES.COOKIE);
135
+
136
+ // might be helpful for clients to send add'l values in event arg
137
+ // - stackOpts: elide frames from function ref in client instrumentation
138
+ // - signature: rather than create shared one in the handler
139
+ // - or stack snapshot function - could share among SourceEvents
140
+ // - call context to share among SourceEvents
141
+ agentEmitter.on('assess.source', ({ obj, prop, type, signature }) => {
142
+ if (!signature) {
143
+ signature = new Signature({
144
+ moduleName: 'Object',
145
+ methodName: 'getter',
146
+ args: [prop],
147
+ return: 'String',
148
+ isModule: false,
149
+ });
150
+ }
151
+
152
+ new SourceEventHandler({
133
153
  config,
134
154
  exclusions,
135
- QUERYSTRING,
136
- obj,
155
+ signature,
156
+ type,
157
+ stackOpts: undefined,
158
+ snapshot: undefined,
159
+ }).handle({ obj, prop });
160
+ });
161
+ },
162
+
163
+ /**
164
+ * Checks whether exclusions settings should prevent wrapping of the untrusted
165
+ * data in membrane. If we should wrap, we create a membrane and provide it with
166
+ * active input exclusions to modify onString behaviors.
167
+ * @param {object} config agent config used to create membrane
168
+ * @param {ExclusionFactory} exclusions exclusions applicable to current req url
169
+ * @param {string} type source type
170
+ * @param {*} obj proxy target
171
+ * @param {string} prop property name
172
+ */
173
+ handleSourceEvent(config, exclusions, type, obj, prop) {
174
+ const req = AsyncStorage.get(KEYS.REQ);
175
+ // NODE-1431: prevents crashing when req is not present in AsyncStorage.
176
+ if (!req) {
177
+ logger.error(
178
+ 'failed to handle source event for type: %s, property: %s; req not present in async storage',
179
+ type,
137
180
  prop
138
181
  );
182
+ return;
139
183
  }
140
184
 
141
- agentEmitter.emit('assess.source', {
142
- obj,
143
- prop,
144
- data: obj[prop],
145
- type: QUERYSTRING
146
- });
147
- });
148
-
149
- agentEmitter.on('assess.cookies', (obj, prop) => {
150
- if (isLazyTrackingEnabled) {
151
- return sources.handleSourceEvent(config, exclusions, COOKIE, obj, prop);
152
- }
153
-
154
- agentEmitter.emit('assess.source', {
155
- obj,
156
- prop,
157
- type: COOKIE
158
- });
159
- });
160
-
161
- // might be helpful for clients to send add'l values in event arg
162
- // - stackOpts: elide frames from function ref in client instrumentation
163
- // - signature: rather than create shared one in the handler
164
- // - or stack snapshot function - could share among SourceEvents
165
- // - call context to share among SourceEvents
166
- agentEmitter.on('assess.source', ({ obj, prop, type, signature }) => {
167
- if (!signature) {
168
- signature = new Signature({
169
- moduleName: 'Object',
170
- methodName: 'getter',
171
- args: [prop],
172
- return: 'String',
173
- isModule: false
174
- });
185
+ const { pathname } = parseurl(req);
186
+ const urlExclusions = exclusions
187
+ .getUrlExclusions('assess')
188
+ .filter((e) => e.matchesUrl(pathname));
189
+ const inputExclusions = exclusions
190
+ .getInputExclusions('assess')
191
+ .filter((e) => e.matchesUrl(pathname) && e.appliesToInputType(type));
192
+
193
+ if (
194
+ this.exclusionSourceEventFilter({
195
+ type,
196
+ urlExclusions,
197
+ inputExclusions,
198
+ })
199
+ ) {
200
+ return;
175
201
  }
176
202
 
177
- new SourceEventHandler({
178
- config,
179
- exclusions,
180
- signature,
181
- type,
182
- stackOpts: undefined,
183
- snapshot: undefined
184
- }).handle({ obj, prop });
185
- });
186
- };
187
-
188
- /**
189
- * Checks whether exclusions settings should prevent wrapping of the untrusted
190
- * data in membrane. If we should wrap, we create a membrane and provide it with
191
- * active input exclusions to modify onString behaviors.
192
- * @param {object} config agent config used to create membrane
193
- * @param {ExclusionFactory} exclusions exclusions applicable to current req url
194
- * @param {string} type source type
195
- * @param {*} obj proxy target
196
- * @param {string} prop property name
197
- */
198
- sources.handleSourceEvent = function(config, exclusions, type, obj, prop) {
199
- const req = AsyncStorage.get(KEYS.REQ);
200
- // NODE-1431: prevents crashing when req is not present in AsyncStorage.
201
- if (!req) {
202
- logger.error(
203
- 'failed to handle source event for type: %s, property: %s; req not present in async storage',
203
+ const sourceParams = {
204
204
  type,
205
- prop
205
+ object: obj[prop],
206
+ constructorOpt: agentEmitter.emit,
207
+ inputExclusions,
208
+ };
209
+
210
+ const membrane = new SourceMembrane(config, sourceParams);
211
+ this.track(type.toLowerCase(), obj, prop, membrane);
212
+ },
213
+
214
+ /**
215
+ * Event filter function returns `false` if source event shouldn't be published.
216
+ * If there is a URL Exclusion for all rules, then don't bother tracking inputs.
217
+ * Otherwise, we have logic in TSReporter to skip reporting for URL Exclusions
218
+ * for specific rules.
219
+ * And some Input Exclusions apply broadly to the type. If they also apply to
220
+ * all rules we can skip the source event outright.
221
+ * @param {object} params
222
+ * @param {string} params.type
223
+ * @param {object[]} params.urlExclusions
224
+ * @param {object[]} params.inputExclusions
225
+ * @returns {boolean}
226
+ */
227
+ exclusionSourceEventFilter({ type, urlExclusions, inputExclusions }) {
228
+ const urlMatch = urlExclusions.find((e) => e.appliesToAllAssessRules());
229
+ if (urlMatch) {
230
+ logExclusionMessage(type, urlMatch);
231
+ return true;
232
+ }
233
+ // `isNamed` is false for exclusion types such as BODY and QUERYSTRING which
234
+ // apply to the entire set of params not only those by a specified name. Also
235
+ // is true if the input name is set to wildcard "*" meaning it should apply to
236
+ // all values. In each case there's no need to wrap if all rules are excluded.
237
+ const inputMatch = inputExclusions.find(
238
+ (e) => e.appliesToAllAssessRules() && !e.isNamed
206
239
  );
207
- return;
208
- }
209
-
210
- const { pathname } = parseurl(req);
211
- const urlExclusions = exclusions
212
- .getUrlExclusions('assess')
213
- .filter((e) => e.matchesUrl(pathname));
214
- const inputExclusions = exclusions
215
- .getInputExclusions('assess')
216
- .filter((e) => e.matchesUrl(pathname) && e.appliesToInputType(type));
217
-
218
- if (
219
- sources.exclusionSourceEventFilter({
220
- type,
221
- urlExclusions,
222
- inputExclusions
223
- })
224
- ) {
225
- return;
226
- }
227
-
228
- const sourceParams = {
229
- type,
230
- object: obj[prop],
231
- constructorOpt: agentEmitter.emit,
232
- inputExclusions
233
- };
234
-
235
- const membrane = new SourceMembrane(config, sourceParams);
236
- sources.track(type.toLowerCase(), obj, prop, membrane);
237
- };
238
-
239
- /**
240
- * Event filter function returns `false` if source event shouldn't be published.
241
- * If there is a URL Exclusion for all rules, then don't bother tracking inputs.
242
- * Otherwise, we have logic in TSReporter to skip reporting for URL Exclusions
243
- * for specific rules.
244
- * And some Input Exclusions apply broadly to the type. If they also apply to
245
- * all rules we can skip the source event outright.
246
- * @param {object} params
247
- * @param {string} params.type
248
- * @param {object[]} params.urlExclusions
249
- * @param {object[]} params.inputExclusions
250
- * @returns {boolean}
251
- */
252
- sources.exclusionSourceEventFilter = function({
253
- type,
254
- urlExclusions,
255
- inputExclusions
256
- }) {
257
- const urlMatch = urlExclusions.find((e) => e.appliesToAllAssessRules());
258
- if (urlMatch) {
259
- logExclusionMessage(type, urlMatch);
260
- return true;
261
- }
262
- // `isNamed` is false for exclusion types such as BODY and QUERYSTRING which
263
- // apply to the entire set of params not only those by a specified name. Also
264
- // is true if the input name is set to wildcard "*" meaning it should apply to
265
- // all values. In each case there's no need to wrap if all rules are excluded.
266
- const inputMatch = inputExclusions.find(
267
- (e) => e.appliesToAllAssessRules() && !e.isNamed
268
- );
269
- if (inputMatch) {
270
- logExclusionMessage(type, inputMatch);
271
- return true;
272
- }
240
+ if (inputMatch) {
241
+ logExclusionMessage(type, inputMatch);
242
+ return true;
243
+ }
273
244
 
274
- return false;
245
+ return false;
246
+ },
275
247
  };
276
-
277
- /**
278
- * Shared logging for when URL/Input exclusions thwart handling of sources.
279
- * @param {string} type the source type e.g. BODY, PARAMETER, HEADER
280
- */
281
- function logExclusionMessage(type, exclusion) {
282
- const { name } = exclusion;
283
- logger.debug(
284
- 'excluding %s inputs from all assess rules (%s)',
285
- type.toLowerCase(),
286
- name
287
- );
288
- }
@@ -376,14 +376,16 @@ class ExpressFramework {
376
376
  createMiddlewareWatchers() {
377
377
  this.useAfter(function ContrastRequestReady(req, res, next) {
378
378
  setFrameworkRequest(req, ExpressRequest);
379
+
379
380
  agentEmitter.emit(
380
381
  EVENTS.REQUEST_READY,
381
382
  req,
382
383
  res,
383
384
  INPUT_TYPES.QUERYSTRING,
384
385
  );
386
+
385
387
  next();
386
- }, 'query');
388
+ }, 'expressInit');
387
389
 
388
390
  // ... multer(multi-part form uploads) ...........................
389
391
  this.useAfter(function ContrastMultiPartParsed(req, res, next) {
@@ -455,7 +457,7 @@ class ExpressFramework {
455
457
  const params = new Object(this.params);
456
458
  if (Object.keys(params).length) {
457
459
  agentEmitter.emit(
458
- EVENTS.PARAM_PARSED,
460
+ EVENTS.PARAMS_PARSED,
459
461
  req,
460
462
  res,
461
463
  INPUT_TYPES.URL_PARAMETER,
@@ -14,7 +14,7 @@ Copyright: 2022 Contrast Security, Inc
14
14
  */
15
15
  'use strict';
16
16
 
17
- const distringuish = require('@contrast/distringuish-prebuilt');
17
+ const distringuish = require('@contrast/distringuish');
18
18
  const logger = require('../core/logger')('contrast:hooks');
19
19
 
20
20
  // some functions in the standard library allow users to specify a string
@@ -74,7 +74,7 @@ function hardPatchEncoding(obj, objName, methodName, strIndex) {
74
74
  // check each possible index (encoding can be a number of arguments and)
75
75
  // it depends on the function.
76
76
  // force to desired encoding by decoding to a buffer and then re-encoding.
77
- if (distringuish.isExternal(args[strIndex])) {
77
+ if (typeof args[strIndex] === 'string' && distringuish.isExternal(args[strIndex])) {
78
78
  args[strIndex] = copy(args[strIndex]);
79
79
  }
80
80
  } catch (err) {
@@ -38,6 +38,7 @@ const execFile = util.promisify(require('child_process').execFile);
38
38
  * @param {*} logger
39
39
  * @returns {Promise<Result>}
40
40
  */
41
+ // eslint-disable-next-line complexity
41
42
  module.exports = async function listInstalled(cwd, logger) {
42
43
  const execFileOpts = {
43
44
  cwd,
@@ -28,7 +28,7 @@ class ExpressSources {
28
28
  agentEmitter.on(EVENTS.REQUEST_READY, ExpressSources.handleReady);
29
29
  agentEmitter.on(EVENTS.BODY_PARSED, ExpressSources.handleBody);
30
30
  agentEmitter.on(EVENTS.COOKIES_PARSED, ExpressSources.handleCookies);
31
- agentEmitter.on(EVENTS.PARAM_PARSED, ExpressSources.handleParams);
31
+ agentEmitter.on(EVENTS.PARAMS_PARSED, ExpressSources.handleParams);
32
32
  }
33
33
 
34
34
  static handleReady(req, res, inputType) {
@@ -169,7 +169,7 @@ class Speedracer {
169
169
  this.logger.info('starting contrast-service');
170
170
  this.startTime = Date.now();
171
171
 
172
- const speedracerPath = path.resolve(
172
+ let speedracerPath = path.resolve(
173
173
  __dirname,
174
174
  '..',
175
175
  '..',
@@ -179,6 +179,10 @@ class Speedracer {
179
179
  `contrast-service-${process.platform}-${process.arch}`
180
180
  );
181
181
 
182
+ if (process.platform === 'win32') {
183
+ speedracerPath = `${speedracerPath}.exe`;
184
+ }
185
+
182
186
  try {
183
187
  fs.statSync(speedracerPath);
184
188
  } catch (error) {
package/lib/tracker.js CHANGED
@@ -12,188 +12,137 @@ Copyright: 2022 Contrast Security, Inc
12
12
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
13
  way not consistent with the End User License Agreement.
14
14
  */
15
- /**
16
- * Tracker for objects we want to persist contrastProperties for.
17
- *
18
- * @module lib/tracker
19
- */
20
15
  'use strict';
21
16
 
22
17
  const logger = require('./core/logger')('contrast:tracker');
23
- const distringuish = require('@contrast/distringuish-prebuilt');
24
-
25
- const defaultContrastProperties = {
26
- get tracked() {
27
- return false;
28
- },
29
- set tracked(arg) {
30
- logger.warn('tracked assigned to default contrastProperties');
31
- },
32
- get tagRanges() {
33
- logger.warn('tagRanges referenced from default contrastProperties');
34
- return [];
35
- },
36
- set tagRanges(arg) {
37
- logger.warn('tagRanges assigned to default contrastProperties');
38
- },
39
- get event() {
40
- logger.warn('event referenced from default contrastProperties');
41
- return null;
42
- },
43
- set event(arg) {
44
- logger.warn('event assigned to default contrastProperties');
45
- }
46
- };
47
-
48
- // i'm not sure why this is a class. there are no methods, and externalized
49
- // strings don't have an instance of the class; they have an object with the
50
- // same property names.
51
- class ContrastProperties {
52
- constructor() {
53
- this.event = null;
54
- this.tagRanges = [];
55
- this.tracked = true;
56
- }
57
- // this is used to populate the object created by externalize.
58
- static populate(obj) {
59
- obj.event = null;
60
- obj.tagRanges = [];
61
- obj.tracked = true;
62
- }
63
- }
18
+ const distringuish = require('@contrast/distringuish');
64
19
 
65
20
  class Tracker {
66
21
  constructor() {
67
- // map target --> ContrastProperties
22
+ /** @type {WeakMap<String, ContrastProperties>} */
68
23
  this.metadata = new WeakMap();
69
24
  }
70
25
 
26
+ /**
27
+ * Externalizes a given primitive string, attaching a ContrastProperties
28
+ * object. Returns null if the string is unable to be externalized.
29
+ * @param {string} str
30
+ * @returns {{ str: str | string, props: ContrastProperties} | null}
31
+ */
71
32
  trackString(str) {
72
- if (str.length === 0) {
73
- return str;
33
+ let props = distringuish.getProperties(str);
34
+ if (props) {
35
+ return { str, props };
74
36
  }
75
37
 
76
- // XXX: this is the closest we have to a dedup.
77
- // it may be kind of expensive. we need to consider whether or not
78
- // this is worthwhile
79
- if (this.getData(str)) {
80
- return str;
38
+ str = distringuish.externalize(str);
39
+
40
+ if (typeof str === 'number') {
41
+ switch (str) {
42
+ case distringuish.ExternalizeErrorCode.ZERO_LENGTH_STRING:
43
+ logger.warn('Cannot externalize and track empty strings.'); // we should never see this error
44
+ return null;
45
+ case distringuish.ExternalizeErrorCode.INVALID_STRING_ENCODING:
46
+ logger.error('Unable to externalize non-two-byte strings.');
47
+ return null;
48
+ case distringuish.ExternalizeErrorCode.TO_LOCAL_FAILURE:
49
+ logger.error(
50
+ 'An error occurred when creating a new external string.'
51
+ );
52
+ return null;
53
+ case distringuish.ExternalizeErrorCode.EXTERNALIZATION_FAILURE:
54
+ logger.error('The provided string was unable to be externalized.');
55
+ return null;
56
+ default:
57
+ logger.error('An unknown error occurred');
58
+ return null;
59
+ }
81
60
  }
82
61
 
83
- const ext = distringuish.externalize(str);
84
-
85
- // XXX this was causing SourceEvent not to get GC'd, for some reason
86
- // const data = new ContrastProperties(ext, parent, sourceType, parentKey);
87
- const data = new ContrastProperties();
88
- const props = distringuish.getProperties(ext);
89
- Object.assign(props, data);
62
+ props = distringuish.getProperties(str);
63
+ Object.assign(props, {
64
+ event: null,
65
+ tagRanges: [],
66
+ tracked: true,
67
+ });
90
68
 
91
- return ext;
69
+ return { str, props };
92
70
  }
93
71
 
94
- trackStringObject(value) {
95
- if (value.length === 0) {
96
- return value;
97
- }
98
-
99
- // no duplicates
100
- if (this.metadata.has(value)) {
101
- return value;
72
+ /**
73
+ * Tracks a given String object using the `metadata` WeakMap.
74
+ * @param {String} str
75
+ * @returns {str}
76
+ */
77
+ trackStringObject(str) {
78
+ if (!this.metadata.has(str)) {
79
+ this.metadata.set(str, {
80
+ event: null,
81
+ tagRanges: [],
82
+ tracked: true,
83
+ });
102
84
  }
103
85
 
104
- this.metadata.set(value, new ContrastProperties());
105
-
106
- return value;
86
+ return { str, props: this.metadata.get(str) };
107
87
  }
108
88
 
109
-
110
89
  /**
111
- * Associate properties with a string. Returns null if str is not a string,
112
- * is a zero-length string, or any internal error takes place.
113
- *
114
- * @param {*} str a value to track.
115
- * @returns {Object|null} {str, props} or null on error.
90
+ * Return the properties associated with a given string. Returns null if the
91
+ * string has not previously been tracked.
92
+ * @param {string | String} str
93
+ * @return {ContrastProperties | null}
116
94
  */
117
- track(str) {
95
+ getData(str) {
96
+ let prop;
118
97
  if (typeof str === 'string') {
119
- // is the string already tracked?
120
- let props = distringuish.getProperties(str);
121
- if (props) {
122
- return { str, props };
123
- }
124
-
125
- str = distringuish.externalize(str);
126
- if (!str) {
127
- return null;
128
- }
129
- props = distringuish.getProperties(str);
130
- if (!props) {
131
- return null;
132
- }
133
- ContrastProperties.populate(props);
134
-
135
- return { str, props };
98
+ prop = distringuish.getProperties(str);
99
+ } else if (str instanceof String) {
100
+ prop = this.metadata.get(str);
136
101
  }
137
102
 
138
- if (str instanceof String && str.length) {
139
- // no duplicates
140
- let props = this.metadata.get(str);
141
- if (props) {
142
- return { str, props };
143
- }
144
-
145
- props = new ContrastProperties();
146
- this.metadata.set(str, props);
147
-
148
- return { str, props };
149
- }
150
-
151
- return null;
103
+ return prop || null;
152
104
  }
153
105
 
154
106
  /**
155
- * Return the properties associated with a value or null.
156
- *
157
- * @param {*} str any value
158
- * @return {ContrastProperties|null}
107
+ * Associate properties with a string. Returns null if str is not a string,
108
+ * is a zero-length string, or any internal error takes place.
109
+ * @param {string | String} str
110
+ * @returns {{ str: str | string, props: ContrastProperties} | null}
159
111
  */
160
- getData(str) {
161
- if (typeof str === 'string') {
162
- return distringuish.getProperties(str);
112
+ track(str) {
113
+ if (typeof str === 'string' && str.length > 0) {
114
+ return this.trackString(str);
163
115
  }
164
- if (str instanceof String) {
165
- return this.metadata.get(str) || null;
116
+
117
+ if (str instanceof String && str.length > 0) {
118
+ return this.trackStringObject(str);
166
119
  }
120
+
167
121
  return null;
168
122
  }
169
123
 
170
124
  /**
171
- * Resets a string's tracking metadata to the default contrast properties.
172
- * This will effectively untrack the associated string, but it will still be
173
- * the externalized value.
174
- * @param {object} trackingData A tracked string's metadata
125
+ * Resets a string's tracked data metadata to the default contrast properties.
126
+ * If the string is a primitive, we return a new, non-externalized copy of the
127
+ * string.
128
+ * If the string was an Object, we remove the properties from the `metadata`
129
+ * WeakMap.
130
+ * @param {string | String} str
131
+ * @returns {str | null}
175
132
  */
176
133
  untrack(str) {
177
134
  if (typeof str === 'string') {
178
- let props = distringuish.getProperties(str);
179
- if (!props) {
180
- return null;
181
- }
182
- // return an untracked version of the string
183
- Object.assign(props, {event: null, tagRanges: [], tracked: false})
184
135
  return distringuish.internalize(str);
185
136
  }
137
+
186
138
  if (str instanceof String) {
187
- if (!this.metadata.delete(str)) {
188
- return null;
189
- }
139
+ this.metadata.delete(str);
190
140
  return str;
191
141
  }
142
+
192
143
  return null;
193
144
  }
194
145
  }
195
146
 
196
- const tracker = new Tracker();
197
- module.exports = tracker;
147
+ module.exports = new Tracker();
198
148
  module.exports.Tracker = Tracker;
199
- module.exports.defaultContrastProperties = defaultContrastProperties;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/agent",
3
- "version": "4.24.2",
3
+ "version": "4.25.1",
4
4
  "description": "Node.js security instrumentation by Contrast Security",
5
5
  "keywords": [
6
6
  "security",
@@ -77,7 +77,7 @@
77
77
  "@babel/traverse": "^7.12.1",
78
78
  "@babel/types": "^7.12.1",
79
79
  "@contrast/agent-lib": "^4.3.0",
80
- "@contrast/distringuish-prebuilt": "^3.2.0",
80
+ "@contrast/distringuish": "^4.0.0",
81
81
  "@contrast/flat": "^4.1.1",
82
82
  "@contrast/fn-inspect": "^3.1.0",
83
83
  "@contrast/protobuf-api": "^3.2.5",
@@ -193,7 +193,7 @@
193
193
  "test": "test"
194
194
  },
195
195
  "engines": {
196
- "node": ">=12.13.0 <13 || >=14.15.0 <15 || >=16.9.1 <17",
196
+ "node": ">=12.13.0 <13 || >=14.15.0 <15 || >=16.9.1 <17 || >=18.7.0 <19",
197
197
  "npm": ">=6.13.7 <7 || >=7.11.0"
198
198
  },
199
199
  "bundleDependencies": [