@contrast/assess 1.51.0 → 1.53.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.
@@ -22,6 +22,7 @@ const {
22
22
  StringPrototypeMatchAll,
23
23
  StringPrototypeReplace,
24
24
  StringPrototypeReplaceAll,
25
+ StringPrototypeSubstring
25
26
  }
26
27
  } = require('@contrast/common');
27
28
  const {
@@ -34,6 +35,7 @@ const { patchType } = require('../../common');
34
35
  module.exports = function(core) {
35
36
  const {
36
37
  patcher,
38
+ scopes,
37
39
  assess: {
38
40
  getPropagatorContext,
39
41
  eventFactory: { createPropagationEvent },
@@ -85,8 +87,7 @@ module.exports = function(core) {
85
87
  // aren't valid, e.g., $0, $<name> when the pattern is a string.
86
88
  replacement = String(data._replacement);
87
89
 
88
- const matches = [...StringPrototypeMatchAll.call(replacement, patternIsRE ? RE_REPS : STR_REPS)];
89
- let trackedReplacementsDone = 0;
90
+ const matches = StringPrototypeMatchAll.call(replacement, patternIsRE ? RE_REPS : STR_REPS);
90
91
 
91
92
  for (const m of matches) {
92
93
  let substitution;
@@ -113,7 +114,7 @@ module.exports = function(core) {
113
114
  substitutionDone = true;
114
115
  } else if (m[1][0] === '<') {
115
116
  // named group
116
- const groupName = m[1].substring(1, m[1].length - 1);
117
+ const groupName = StringPrototypeSubstring.call(m[1], 1, m[1].length - 1);
117
118
  if (parsedArgs.groups[groupName] === undefined && groupName in parsedArgs.groups) {
118
119
  // remove $<groupName> from the replacement pattern. N.B. this
119
120
  // also could mess up if the replacement text containts text that
@@ -130,6 +131,7 @@ module.exports = function(core) {
130
131
  //
131
132
  // any idea what order $&'` should be in from a most-common to least-
132
133
  // common perspective? nfi.
134
+ let substitutionTags;
133
135
  if (!substitutionDone) {
134
136
  if (m[1] === '$') {
135
137
  // this could actually be tracked but in order to do this "right"
@@ -137,15 +139,20 @@ module.exports = function(core) {
137
139
  // the second $ that is tracked. so punt, and just ignore it, as
138
140
  // originally implemented.
139
141
  substitution = '$';
142
+ data._replacementOffset -= 1;
140
143
  } else if (m[1] === '&') {
141
144
  // replace these '$&' in parsedArgs[0] (replacerArgs[0], i.e., the
142
145
  // match). if tracked, handle it. do so by index, so we don't have to
143
146
  // call replace again? e.g., string[m.index..m.index+2]
144
147
  substitution = parsedArgs[0];
145
148
  } else if (m[1] === '`') {
146
- substitution = parsedArgs.input.substring(0, parsedArgs.index);
149
+ const info = tracker.getData(parsedArgs.input);
150
+ substitution = StringPrototypeSubstring.call(parsedArgs.input, 0, parsedArgs.index);
151
+ substitutionTags = createSubsetTags(info.tags, parsedArgs.index, substitution.length);
147
152
  } else if (m[1] === "'") {
148
- substitution = parsedArgs.input.substring(parsedArgs.index + parsedArgs[0].length);
153
+ const info = tracker.getData(parsedArgs.input);
154
+ substitution = StringPrototypeSubstring.call(parsedArgs.input, parsedArgs.index + parsedArgs[0].length);
155
+ substitutionTags = createSubsetTags(info.tags, parsedArgs.index, substitution.length);
149
156
  } // else {
150
157
  // throw new Error('how can it have matched RE and gotten here?');
151
158
  // }
@@ -154,15 +161,13 @@ module.exports = function(core) {
154
161
  // is manipulating the output, even if it is just duplicating what
155
162
  // might be tracked or untracked input. does that count?
156
163
  }
157
- // if the substitution is tracked just use heavy-weight replacer.
158
- if (trackedReplacementsDone || tracker.isTracked(substitution)) {
159
- replacement = replacement.replace(m[0], substitution);
160
- trackedReplacementsDone += 1;
161
- } else {
162
- replacement = StringPrototypeReplace.call(replacement, m[0], substitution);
164
+ replacement = StringPrototypeReplace.call(replacement, m[0], substitution);
165
+ if (!substitutionTags && tracker.getData(substitution)) substitutionTags = tracker.getData(substitution)?.tags;
166
+ if (substitutionTags) {
167
+ data._replacementTags = createAppendTags(data._replacementTags || {}, substitutionTags, m.index + data._replacementOffset);
168
+ data._replacementOffset += (substitution.length - m[0].length);
163
169
  }
164
170
  }
165
-
166
171
  }
167
172
 
168
173
  // coerce the replacement to a string, e.g., null => 'null'
@@ -173,7 +178,7 @@ module.exports = function(core) {
173
178
  data._history.add(data._replacementInfo);
174
179
  }
175
180
 
176
- return { replacement, replacementInfo: data._replacementInfo };
181
+ return { replacement, replacementTags: data._replacementTags || data._replacementInfo?.tags };
177
182
  }
178
183
 
179
184
  function getReplacer(data) {
@@ -183,14 +188,14 @@ module.exports = function(core) {
183
188
  const { index, input } = parsedArgs;
184
189
 
185
190
  const { _accumOffset, _accumTags } = data;
186
- const { replacement, replacementInfo } = getReplacementInfo(data, args, parsedArgs);
191
+ const { replacement, replacementTags } = getReplacementInfo(data, args, parsedArgs);
187
192
 
188
193
  const preTags = createSubsetTags(_accumTags, 0, _accumOffset + index);
189
194
  const postTags = createSubsetTags(_accumTags, _accumOffset + index + match.length, input.length - index - match.length);
190
195
  data._accumOffset += (replacement.length - match.length);
191
- if (preTags || postTags || replacementInfo) {
196
+ if (preTags || postTags || replacementTags) {
192
197
  data._accumTags = createAppendTags(
193
- createAppendTags(preTags, replacementInfo?.tags, _accumOffset + index),
198
+ createAppendTags(preTags, replacementTags, _accumOffset + index),
194
199
  postTags,
195
200
  data._accumOffset + index + match.length
196
201
  );
@@ -204,12 +209,13 @@ module.exports = function(core) {
204
209
  return core.assess.dataflow.propagation.stringInstrumentation.replace = {
205
210
  install() {
206
211
  const name = 'String.prototype.replace';
212
+ const store = { name, lock: true };
207
213
  patcher.patch(String.prototype, 'replace', {
208
214
  name,
209
215
  patchType,
210
216
  usePerf: 'sync',
211
- pre(data) {
212
- if (!getPropagatorContext()) return;
217
+ around(next, data) {
218
+ if (!getPropagatorContext()) return next();
213
219
 
214
220
  // setup state
215
221
  data._objInfo = tracker.getData(data.obj);
@@ -218,22 +224,20 @@ module.exports = function(core) {
218
224
  data._history = data._objInfo ? new Set([data._objInfo]) : new Set();
219
225
  data._accumTags = data._objInfo?.tags || {};
220
226
  data._accumOffset = 0;
227
+ data._replacementOffset = 0;
221
228
 
222
229
  data.args[1] = getReplacer(data);
223
- },
224
- post(data) {
230
+
231
+ const result = !scopes.instrumentation.isLocked() ? scopes.instrumentation.run(store, next) : next();
232
+
225
233
  if (
226
- !data.result ||
227
- // todo: can we reuse this optimization in other propagators? e.g those performing substring-like operations
234
+ !result ||
228
235
  !data._accumTags?.[UNTRUSTED] ||
229
- !!tracker.getData(data.result)
230
- ) return;
236
+ !!tracker.getData(result) ||
237
+ data.obj === result
238
+ ) return result;
231
239
 
232
- if (data.obj === data.result) {
233
- return;
234
- }
235
-
236
- const { obj, args: origArgs, result, hooked, orig } = data;
240
+ const { obj, args: origArgs, hooked, orig } = data;
237
241
  const args = [];
238
242
  if (tracker.getData(origArgs[0])) {
239
243
  args.push({ tracked: true, value: origArgs[0] });
@@ -274,9 +278,7 @@ module.exports = function(core) {
274
278
 
275
279
  const { extern } = tracker.track(result, event);
276
280
 
277
- if (extern) {
278
- data.result = extern;
279
- }
281
+ return extern;
280
282
  }
281
283
  });
282
284
  },
@@ -86,7 +86,7 @@ module.exports = function(core) {
86
86
 
87
87
  unvalidatedRedirect.install = function() {
88
88
  const name = 'fastify.Reply.prototype.redirect';
89
- depHooks.resolve({ name: 'fastify', version: '>=3 <5', file: 'lib/reply' }, (Reply) => {
89
+ depHooks.resolve({ name: 'fastify', version: '>=3 <6', file: 'lib/reply' }, (Reply) => {
90
90
  patcher.patch(Reply.prototype, 'redirect', {
91
91
  name,
92
92
  patchType,
@@ -31,7 +31,7 @@ module.exports = function (core) {
31
31
 
32
32
  const source = sources.fastifyInstrumentation.fastify = {
33
33
  install() {
34
- depHooks.resolve({ name: 'fastify', version: '>=3.2.0 <5' }, (fastify) => patcher.patch(fastify, {
34
+ depHooks.resolve({ name: 'fastify', version: '>=3.2.0 <6' }, (fastify) => patcher.patch(fastify, {
35
35
  name: 'fastify.constructor',
36
36
  patchType,
37
37
  post({ result: server, funcKey }) {
@@ -15,7 +15,8 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { InputType, empties, primordials: { StringPrototypeMatch } } = require('@contrast/common');
18
+ const { Event, InputType, empties, primordials: { StringPrototypeMatch } } = require('@contrast/common');
19
+ const { ConfigSource } = require('@contrast/config');
19
20
  const { Core } = require('@contrast/core/lib/ioc/core');
20
21
  const ANNOTATION_REGEX = /^(A|O|R|P|P\d+)$/;
21
22
  const SOURCE_EVENT_MSG = 'Source event not created: %s';
@@ -33,6 +34,15 @@ module.exports = Core.makeComponent({
33
34
 
34
35
  const eventFactory = core.assess.eventFactory = {};
35
36
 
37
+ function computeStacktracesConfig() {
38
+ const forceSink = config.getEffectiveValue('server.environment') === 'PRODUCTION' &&
39
+ config.getEffectiveSource('assess.stacktraces') === ConfigSource.DEFAULT_VALUE;
40
+
41
+ return forceSink ? 'SINK' : config.getEffectiveValue('assess.stacktraces');
42
+ }
43
+
44
+ eventFactory.stacktraces = computeStacktracesConfig();
45
+
36
46
  eventFactory.createdEvents = new WeakSet();
37
47
 
38
48
  eventFactory.createSourceEvent = function(data = {}) {
@@ -91,7 +101,7 @@ module.exports = Core.makeComponent({
91
101
  return null;
92
102
  }
93
103
 
94
- if (config.assess.stacktraces === 'ALL') {
104
+ if (eventFactory.stacktraces === 'ALL') {
95
105
  data.stack = createSnapshot(data.stacktraceOpts)();
96
106
  } else {
97
107
  data.stack = empties.ARRAY;
@@ -133,7 +143,7 @@ module.exports = Core.makeComponent({
133
143
  return null;
134
144
  }
135
145
 
136
- if (config.assess.stacktraces !== 'NONE') {
146
+ if (eventFactory.stacktraces !== 'NONE') {
137
147
  data.stack = createSnapshot(data.stacktraceOpts)();
138
148
  } else {
139
149
  data.stack = empties.ARRAY;
@@ -161,7 +171,7 @@ module.exports = Core.makeComponent({
161
171
  logger.debug('malformed or missing sink event source field: %s', data.name);
162
172
  return null;
163
173
  }
164
- if (config.assess.stacktraces !== 'NONE') {
174
+ if (eventFactory.stacktraces !== 'NONE') {
165
175
  data.stack = createSnapshot(data.stacktraceOpts)();
166
176
  } else {
167
177
  data.stack = empties.ARRAY;
@@ -202,7 +212,7 @@ module.exports = Core.makeComponent({
202
212
  logger.debug('malformed or missing sink event source field: %s', data.name);
203
213
  return null;
204
214
  }
205
- if (config.assess.stacktraces !== 'NONE') {
215
+ if (eventFactory.stacktraces !== 'NONE') {
206
216
  data.stack = createSnapshot(data.stacktraceOpts)();
207
217
  } else {
208
218
  data.stack = empties.ARRAY;
@@ -214,6 +224,12 @@ module.exports = Core.makeComponent({
214
224
  return data;
215
225
  };
216
226
 
227
+
228
+ // update computed config value if there are updates
229
+ core.messages.on(Event.SERVER_SETTINGS_UPDATE, () => {
230
+ eventFactory.stacktraces = computeStacktracesConfig();
231
+ });
232
+
217
233
  return eventFactory;
218
234
  }
219
235
  });
@@ -15,6 +15,8 @@
15
15
  // @ts-check
16
16
  'use strict';
17
17
 
18
+ const { Core } = require('@contrast/core/lib/ioc/core');
19
+
18
20
  /** @typedef {import('@contrast/assess').SourceContext} SourceContext */
19
21
 
20
22
  /**
@@ -25,7 +27,12 @@
25
27
  * assess: import('@contrast/assess').Assess;
26
28
  * }} core
27
29
  */
28
- module.exports = function(core) {
30
+ module.exports = Core.makeComponent({
31
+ name: 'assess.getSourceContext',
32
+ factory
33
+ });
34
+
35
+ function factory(core) {
29
36
  const {
30
37
  config,
31
38
  scopes: { sources, instrumentation },
@@ -109,4 +116,4 @@ module.exports = function(core) {
109
116
 
110
117
  return ctx;
111
118
  };
112
- };
119
+ }
package/lib/index.js CHANGED
@@ -63,7 +63,7 @@ module.exports = function assess(core) {
63
63
  require('./get-policy')(core);
64
64
  core.initComponentSync(require('./make-source-context'));
65
65
  require('./rule-scopes')(core);
66
- require('./get-source-context')(core);
66
+ core.initComponentSync(require('./get-source-context'));
67
67
  core.initComponentSync(require('./event-factory'));
68
68
 
69
69
  // various Assess features
@@ -26,75 +26,76 @@ const { Core } = require('@contrast/core/lib/ioc/core');
26
26
  */
27
27
  module.exports = Core.makeComponent({
28
28
  name: 'assess.makeSourceContext',
29
- factory(core) {
30
- const { assess, logger } = core;
29
+ factory,
30
+ });
31
31
 
32
- /**
33
- * @returns {import('@contrast/assess').SourceContext}
34
- */
35
- return core.assess.makeSourceContext = function(sourceData) {
36
- try {
32
+ function factory(core) {
33
+ const { assess, logger } = core;
37
34
 
38
- const ctx = sourceData.store.assess = {
39
- // default policy to `null` until it is set later below. this will cause
40
- // all instrumentation to short-circuit, see `./get-source-context.js`.
41
- policy: null,
42
- };
35
+ /**
36
+ * @returns {import('@contrast/assess').SourceContext}
37
+ */
38
+ return core.assess.makeSourceContext = function(sourceData) {
39
+ try {
43
40
 
44
- if (!core.config.getEffectiveValue('assess.enable')) {
45
- return ctx;
46
- }
41
+ const ctx = sourceData.store.assess = {
42
+ // default policy to `null` until it is set later below. this will cause
43
+ // all instrumentation to short-circuit, see `./get-source-context.js`.
44
+ policy: null,
45
+ };
47
46
 
48
- // todo: how to handle non-HTTP sources
49
- const { incomingMessage: req } = sourceData;
47
+ if (!core.config.getEffectiveValue('assess.enable')) {
48
+ return ctx;
49
+ }
50
50
 
51
- // minimally process the request data for sampling and exclusions.
52
- // more request fields will be appended in final result below.
53
- let uriPath;
54
- let queries;
55
- const idx = req.url.indexOf('?');
56
- if (idx >= 0) {
57
- uriPath = StringPrototypeSlice.call(req.url, 0, idx);
58
- queries = StringPrototypeSlice.call(req.url, idx + 1);
59
- } else {
60
- uriPath = req.url;
61
- queries = '';
62
- }
63
- ctx.reqData = {
64
- method: req.method,
65
- uriPath,
66
- queries,
67
- };
51
+ // todo: how to handle non-HTTP sources
52
+ const { incomingMessage: req } = sourceData;
68
53
 
69
- // check whether sampling allows processing
70
- ctx.sampleInfo = assess.sampler?.getSampleInfo(sourceData) ?? null;
71
- if (ctx.sampleInfo?.canSample === false) return ctx;
54
+ // minimally process the request data for sampling and exclusions.
55
+ // more request fields will be appended in final result below.
56
+ let uriPath;
57
+ let queries;
58
+ const idx = req.url.indexOf('?');
59
+ if (idx >= 0) {
60
+ uriPath = StringPrototypeSlice.call(req.url, 0, idx);
61
+ queries = StringPrototypeSlice.call(req.url, idx + 1);
62
+ } else {
63
+ uriPath = req.url;
64
+ queries = '';
65
+ }
66
+ ctx.reqData = {
67
+ method: req.method,
68
+ uriPath,
69
+ queries,
70
+ };
72
71
 
73
- // set policy - can be returned as `null` if request is url-excluded.
74
- ctx.policy = assess.getPolicy(ctx.reqData);
75
- if (!ctx.policy) return ctx;
72
+ // check whether sampling allows processing
73
+ ctx.sampleInfo = assess.sampler?.getSampleInfo(sourceData) ?? null;
74
+ if (ctx.sampleInfo?.canSample === false) return ctx;
76
75
 
77
- // build remaining reqData
78
- ctx.reqData.headers = { ...req.headers }; // copy to avoid storing tracked values
79
- ctx.reqData.ip = req.socket.remoteAddress;
80
- ctx.reqData.httpVersion = req.httpVersion;
81
- if (ctx.reqData.headers['content-type'])
82
- ctx.reqData.contentType = StringPrototypeToLowerCase.call(ctx.reqData.headers['content-type']);
76
+ // set policy - can be returned as `null` if request is url-excluded.
77
+ ctx.policy = assess.getPolicy(ctx.reqData);
78
+ if (!ctx.policy) return ctx;
83
79
 
84
- return {
85
- ...ctx,
86
- propagationEventsCount: 0,
87
- sourceEventsCount: 0,
88
- responseData: {},
89
- ruleState: {},
90
- };
91
- } catch (err) {
92
- logger.error(
93
- { err },
94
- 'unable to construct assess store. assess will be disabled for request.'
95
- );
96
- return null;
97
- }
98
- };
99
- }
100
- });
80
+ // build remaining reqData
81
+ ctx.reqData.headers = { ...req.headers }; // copy to avoid storing tracked values
82
+ ctx.reqData.ip = req.socket.remoteAddress;
83
+ ctx.reqData.httpVersion = req.httpVersion;
84
+ if (ctx.reqData.headers['content-type'])
85
+ ctx.reqData.contentType = StringPrototypeToLowerCase.call(ctx.reqData.headers['content-type']);
86
+
87
+ ctx.propagationEventsCount = 0;
88
+ ctx.sourceEventsCount = 0;
89
+ ctx.responseData = {};
90
+ ctx.ruleState = {};
91
+
92
+ return ctx;
93
+ } catch (err) {
94
+ logger.error(
95
+ { err },
96
+ 'unable to construct assess store. assess will be disabled for request.'
97
+ );
98
+ return null;
99
+ }
100
+ };
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/assess",
3
- "version": "1.51.0",
3
+ "version": "1.53.0",
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,16 +21,16 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@contrast/common": "1.32.0",
24
- "@contrast/config": "1.44.0",
25
- "@contrast/core": "1.49.0",
26
- "@contrast/dep-hooks": "1.18.0",
24
+ "@contrast/config": "1.45.0",
25
+ "@contrast/core": "1.50.0",
26
+ "@contrast/dep-hooks": "1.19.0",
27
27
  "@contrast/distringuish": "^5.1.0",
28
- "@contrast/instrumentation": "1.28.0",
29
- "@contrast/logger": "1.22.0",
30
- "@contrast/patcher": "1.21.0",
31
- "@contrast/rewriter": "1.25.0",
32
- "@contrast/route-coverage": "1.39.0",
33
- "@contrast/scopes": "1.19.0",
28
+ "@contrast/instrumentation": "1.29.0",
29
+ "@contrast/logger": "1.23.0",
30
+ "@contrast/patcher": "1.22.0",
31
+ "@contrast/rewriter": "1.26.0",
32
+ "@contrast/route-coverage": "1.41.0",
33
+ "@contrast/scopes": "1.20.0",
34
34
  "semver": "^7.6.0"
35
35
  }
36
36
  }