@contrast/assess 1.54.0 → 1.55.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.
@@ -23,7 +23,8 @@ const {
23
23
  StringPrototypeReplace,
24
24
  StringPrototypeReplaceAll,
25
25
  StringPrototypeSubstring
26
- }
26
+ },
27
+ isString,
27
28
  } = require('@contrast/common');
28
29
  const {
29
30
  createSubsetTags,
@@ -43,170 +44,176 @@ module.exports = function(core) {
43
44
  },
44
45
  } = core;
45
46
 
46
- function parseArgs(args) {
47
- //
48
- // convert replace args (match, p1, p2, ..., matchIdx, str, ?groups) to
49
- // re.exec(string) format [match, g1, g2, ..., index: N, input: '', groups?]
50
- //
51
- const r = [];
52
- let ix = -1;
53
- if (typeof args.at(-1) === 'object') {
54
- r.groups = args.at(-1);
55
- ix = -2;
56
- }
57
- r.input = args.at(ix);
58
- ix -= 1;
59
- r.index = args.at(ix);
47
+ const RE_REPS = /\$(\$|&|`|'|[1-9][0-9]?|<[a-zA-Z0-9_]+>)/g;
48
+ const STR_REPS = /\$(\$|&|`|')/g;
60
49
 
61
- // ix is negative from the end, so add it
62
- ix = args.length + ix;
63
- for (let i = 0; i < ix; i++) {
64
- r.push(args[i]);
65
- }
50
+ const propagator = core.assess.dataflow.propagation.stringInstrumentation.replace = {
51
+ //
52
+ parseArgs(args) {
53
+ //
54
+ // convert replace args (match, p1, p2, ..., matchIdx, str, ?groups) to
55
+ // re.exec(string) format [match, g1, g2, ..., index: N, input: '', groups?]
56
+ //
57
+ const r = [];
58
+ let ix = -1;
59
+ if (typeof args.at(-1) === 'object') {
60
+ r.groups = args.at(-1);
61
+ ix = -2;
62
+ }
63
+ r.input = args.at(ix);
64
+ ix -= 1;
65
+ r.index = args.at(ix);
66
66
 
67
- return r;
68
- }
67
+ // ix is negative from the end, so add it
68
+ ix = args.length + ix;
69
+ for (let i = 0; i < ix; i++) {
70
+ r.push(args[i]);
71
+ }
69
72
 
70
- const RE_REPS = /\$(\$|&|`|'|[1-9][0-9]?|<[a-zA-Z0-9_]+>)/g;
71
- const STR_REPS = /\$(\$|&|`|')/g;
73
+ return r;
74
+ },
75
+ //
76
+ getReplacementInfo(data, replacerArgs, parsedArgs) {
77
+ // patternIsRE is two different flags, both booleans.
78
+ // - set true if the first argument to String.prototype.replace is an RE.
79
+ // - after the $N, $<name> checks, it flags that a substitution was made.
80
+ const patternIsRE = data.args[0] instanceof RegExp;
81
+ let replacement;
82
+ if (typeof data._replacement === 'function') {
83
+ // no special replacements apply to the string returned by a replacement
84
+ // function.
85
+ replacement = data._replacement.call(global, ...replacerArgs);
86
+ } else {
87
+ // if it's not a function then the valid special replacements depend on
88
+ // whether the pattern is a regex or a string. first find substitution
89
+ // patterns present in the replacement string. Don't find patterns that
90
+ // aren't valid, e.g., $0, $<name> when the pattern is a string.
91
+ replacement = String(data._replacement);
72
92
 
73
- function getReplacementInfo(data, replacerArgs, parsedArgs) {
74
- // patternIsRE is two different flags, both booleans.
75
- // - set true if the first argument to String.prototype.replace is an RE.
76
- // - after the $N, $<name> checks, it flags that a substitution was made.
77
- const patternIsRE = data.args[0] instanceof RegExp;
78
- let replacement;
79
- if (typeof data._replacement === 'function') {
80
- // no special replacements apply to the string returned by a replacement
81
- // function.
82
- replacement = data._replacement.call(global, ...replacerArgs);
83
- } else {
84
- // if it's not a function then the valid special replacements depend on
85
- // whether the pattern is a regex or a string. first find substitution
86
- // patterns present in the replacement string. Don't find patterns that
87
- // aren't valid, e.g., $0, $<name> when the pattern is a string.
88
- replacement = String(data._replacement);
93
+ const matches = StringPrototypeMatchAll.call(replacement, patternIsRE ? RE_REPS : STR_REPS);
89
94
 
90
- const matches = StringPrototypeMatchAll.call(replacement, patternIsRE ? RE_REPS : STR_REPS);
95
+ for (const m of matches) {
96
+ let substitution;
97
+ let substitutionDone = false;
98
+ // format of m: ['$`', '`', index: 0, input: string, groups: undefined|{}]
99
+ // if the pattern is a regex, then $1 to $99 and $<name> are valid
100
+ if (patternIsRE) {
101
+ // my guess is $1 to $99 are most likely, after that, who knows? so
102
+ // we check named groups next, because 1) they seem more useful than
103
+ // the other $ patterns and 2) they are in the same patternIsRE test.
104
+ //
105
+ // in any case, the following will be false if m[1][0] is not a number.
106
+ if (m[1][0] >= 1 && m[1][0] <= 9) {
107
+ // a group might not be present, e.g., (pattern)?(a) or (a)|(b).
108
+ if (parsedArgs[m[1]] === undefined) {
109
+ if (m[1] in parsedArgs) {
110
+ // need to remove $m[1] from replacement pattern. N.B. this could
111
+ // mess up if the replacement text contains text that matches a
112
+ // subsequent replacement (either RE or string).
113
+ replacement = StringPrototypeReplaceAll.call(replacement, m[0], '');
114
+ } else {
115
+ // no capture groups in RegExp
116
+ substitution = replacement;
117
+ }
91
118
 
92
- for (const m of matches) {
93
- let substitution;
94
- let substitutionDone = false;
95
- // format of m: ['$`', '`', index: 0, input: string, groups: undefined|{}]
96
- // if the pattern is a regex, then $1 to $99 and $<name> are valid
97
- if (patternIsRE) {
98
- // my guess is $1 to $99 are most likely, after that, who knows? so
99
- // we check named groups next, because 1) they seem more useful than
100
- // the other $ patterns and 2) they are in the same patternIsRE test.
101
- //
102
- // in any case, the following will be false if m[1][0] is not a number.
103
- if (m[1][0] >= 1 && m[1][0] <= 9) {
104
- //const group = Number(m[1]);
105
- // a group might not be present, e.g., (pattern)?(a) or (a)|(b).
106
- if (parsedArgs[m[1]] === undefined && m[1] in parsedArgs) {
107
- // need to remove $m[1] from replacement pattern. N.B. this could
108
- // mess up if the replacement text contains text that matches a
109
- // subsequent replacement (either RE or string).
110
- replacement = StringPrototypeReplaceAll.call(replacement, m[0], '');
111
- continue;
119
+ continue;
120
+ }
121
+ substitution = parsedArgs[m[1]];
122
+ substitutionDone = true;
123
+ } else if (m[1][0] === '<') {
124
+ // named group
125
+ const groupName = StringPrototypeSubstring.call(m[1], 1, m[1].length - 1);
126
+ if (parsedArgs.groups[groupName] === undefined && groupName in parsedArgs.groups) {
127
+ // remove $<groupName> from the replacement pattern. N.B. this
128
+ // also could mess up if the replacement text containts text that
129
+ // matches a subsequent replacement.
130
+ replacement = StringPrototypeReplaceAll.call(replacement, m[0], '');
131
+ continue;
132
+ }
133
+ substitution = parsedArgs.groups[groupName];
134
+ substitutionDone = true;
112
135
  }
113
- substitution = parsedArgs[m[1]];
114
- substitutionDone = true;
115
- } else if (m[1][0] === '<') {
116
- // named group
117
- const groupName = StringPrototypeSubstring.call(m[1], 1, m[1].length - 1);
118
- if (parsedArgs.groups[groupName] === undefined && groupName in parsedArgs.groups) {
119
- // remove $<groupName> from the replacement pattern. N.B. this
120
- // also could mess up if the replacement text containts text that
121
- // matches a subsequent replacement.
122
- replacement = StringPrototypeReplaceAll.call(replacement, m[0], '');
123
- continue;
124
- }
125
- substitution = parsedArgs.groups[groupName];
126
- substitutionDone = true;
127
136
  }
128
- }
129
137
 
130
- // the following are valid whether the pattern is a regex or a string.
131
- //
132
- // any idea what order $&'` should be in from a most-common to least-
133
- // common perspective? nfi.
134
- let substitutionTags;
135
- if (!substitutionDone) {
136
- if (m[1] === '$') {
137
- // this could actually be tracked but in order to do this "right"
138
- // we have to be able to distinguish whether it is the first or
139
- // the second $ that is tracked. so punt, and just ignore it, as
140
- // originally implemented.
141
- substitution = '$';
142
- data._replacementOffset -= 1;
143
- } else if (m[1] === '&') {
144
- // replace these '$&' in parsedArgs[0] (replacerArgs[0], i.e., the
145
- // match). if tracked, handle it. do so by index, so we don't have to
146
- // call replace again? e.g., string[m.index..m.index+2]
147
- substitution = parsedArgs[0];
148
- } else if (m[1] === '`') {
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);
152
- } else if (m[1] === "'") {
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);
156
- } // else {
157
- // throw new Error('how can it have matched RE and gotten here?');
158
- // }
159
- // i'm not sure what the proper handling of this is. if either char
160
- // of the $$&`' sequence is tracked, then something the user input
161
- // is manipulating the output, even if it is just duplicating what
162
- // might be tracked or untracked input. does that count?
163
- }
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);
138
+ // the following are valid whether the pattern is a regex or a string.
139
+ //
140
+ // any idea what order $&'` should be in from a most-common to least-
141
+ // common perspective? nfi.
142
+ let substitutionTags;
143
+ if (!substitutionDone) {
144
+ if (m[1] === '$') {
145
+ // this could actually be tracked but in order to do this "right"
146
+ // we have to be able to distinguish whether it is the first or
147
+ // the second $ that is tracked. so punt, and just ignore it, as
148
+ // originally implemented.
149
+ substitution = '$';
150
+ data._replacementOffset -= 1;
151
+ } else if (m[1] === '&') {
152
+ // replace these '$&' in parsedArgs[0] (replacerArgs[0], i.e., the
153
+ // match). if tracked, handle it. do so by index, so we don't have to
154
+ // call replace again? e.g., string[m.index..m.index+2]
155
+ substitution = parsedArgs[0];
156
+ } else if (m[1] === '`') {
157
+ const info = tracker.getData(parsedArgs.input);
158
+ substitution = StringPrototypeSubstring.call(parsedArgs.input, 0, parsedArgs.index);
159
+ substitutionTags = createSubsetTags(info.tags, parsedArgs.index, substitution.length);
160
+ } else if (m[1] === "'") {
161
+ const info = tracker.getData(parsedArgs.input);
162
+ substitution = StringPrototypeSubstring.call(parsedArgs.input, parsedArgs.index + parsedArgs[0].length);
163
+ substitutionTags = createSubsetTags(info.tags, parsedArgs.index, substitution.length);
164
+ } // else {
165
+ // throw new Error('how can it have matched RE and gotten here?');
166
+ // }
167
+ // i'm not sure what the proper handling of this is. if either char
168
+ // of the $$&`' sequence is tracked, then something the user input
169
+ // is manipulating the output, even if it is just duplicating what
170
+ // might be tracked or untracked input. does that count?
171
+ }
172
+ replacement = StringPrototypeReplace.call(replacement, m[0], substitution);
173
+ if (!substitutionTags && tracker.getData(substitution)) substitutionTags = tracker.getData(substitution)?.tags;
174
+ if (substitutionTags) {
175
+ data._replacementTags = createAppendTags(data._replacementTags || {}, substitutionTags, m.index + data._replacementOffset);
176
+ data._replacementOffset += (substitution.length - m[0].length);
177
+ }
169
178
  }
170
179
  }
171
- }
172
180
 
173
- // coerce the replacement to a string, e.g., null => 'null'
174
- replacement = String(replacement);
181
+ // coerce the replacement to a string, e.g., null => 'null'
182
+ replacement = String(replacement);
175
183
 
176
- data._replacementInfo = tracker.getData(replacement);
177
- if (data._replacementInfo) {
178
- data._history.add(data._replacementInfo);
179
- }
180
-
181
- return { replacement, replacementTags: data._replacementTags || data._replacementInfo?.tags };
182
- }
184
+ data._replacementInfo = tracker.getData(replacement);
185
+ if (data._replacementInfo) {
186
+ data._history.add(data._replacementInfo);
187
+ }
183
188
 
184
- function getReplacer(data) {
185
- return function replacer(...args) {
186
- const parsedArgs = parseArgs(args);
187
- const match = parsedArgs[0];
188
- const { index, input } = parsedArgs;
189
+ return { replacement, replacementTags: data._replacementTags || data._replacementInfo?.tags };
190
+ },
191
+ //
192
+ getReplacer(data) {
193
+ return function replacer(...args) {
194
+ const parsedArgs = propagator.parseArgs(args);
195
+ const match = parsedArgs[0];
196
+ const { index, input } = parsedArgs;
189
197
 
190
- const { _accumOffset, _accumTags } = data;
191
- const { replacement, replacementTags } = getReplacementInfo(data, args, parsedArgs);
198
+ const { _accumOffset, _accumTags } = data;
199
+ const { replacement, replacementTags } = propagator.getReplacementInfo(data, args, parsedArgs);
192
200
 
193
- const preTags = createSubsetTags(_accumTags, 0, _accumOffset + index);
194
- const postTags = createSubsetTags(_accumTags, _accumOffset + index + match.length, input.length - index - match.length);
195
- data._accumOffset += (replacement.length - match.length);
196
- if (preTags || postTags || replacementTags) {
197
- data._accumTags = createAppendTags(
198
- createAppendTags(preTags, replacementTags, _accumOffset + index),
199
- postTags,
200
- data._accumOffset + index + match.length
201
- );
202
- } else {
203
- data._accumTags = {};
204
- }
205
- return replacement;
206
- };
207
- }
201
+ const preTags = createSubsetTags(_accumTags, 0, _accumOffset + index);
202
+ const postTags = createSubsetTags(_accumTags, _accumOffset + index + match.length, input.length - index - match.length);
203
+ data._accumOffset += (replacement.length - match.length);
204
+ if (preTags || postTags || replacementTags) {
205
+ data._accumTags = createAppendTags(
206
+ createAppendTags(preTags, replacementTags, _accumOffset + index),
207
+ postTags,
208
+ data._accumOffset + index + match.length
209
+ );
210
+ } else {
211
+ data._accumTags = {};
212
+ }
213
+ return replacement;
214
+ };
215
+ },
208
216
 
209
- return core.assess.dataflow.propagation.stringInstrumentation.replace = {
210
217
  install() {
211
218
  const name = 'String.prototype.replace';
212
219
  const store = { name, lock: true };
@@ -215,20 +222,27 @@ module.exports = function(core) {
215
222
  patchType,
216
223
  usePerf: 'sync',
217
224
  around(next, data) {
218
- if (!getPropagatorContext()) return next();
225
+ const _next = !scopes.instrumentation.isLocked() ? () => scopes.instrumentation.run(store, next) : next;
226
+
227
+ if (!getPropagatorContext()) return _next();
219
228
 
220
229
  // setup state
221
230
  data._objInfo = tracker.getData(data.obj);
222
231
  data._replacement = data.args[1];
223
- data._replacementType = typeof data._replacement;
224
232
  data._history = data._objInfo ? new Set([data._objInfo]) : new Set();
225
233
  data._accumTags = data._objInfo?.tags || {};
226
234
  data._accumOffset = 0;
227
235
  data._replacementOffset = 0;
228
236
 
229
- data.args[1] = getReplacer(data);
237
+ // bail early if no constituents are tracked
238
+ if (
239
+ !data._objInfo &&
240
+ isString(data.args[1]) &&
241
+ !tracker.getData(data.args[1])
242
+ ) return _next();
230
243
 
231
- const result = !scopes.instrumentation.isLocked() ? scopes.instrumentation.run(store, next) : next();
244
+ data.args[1] = propagator.getReplacer(data);
245
+ const result = _next();
232
246
 
233
247
  if (
234
248
  !result ||
@@ -285,4 +299,6 @@ module.exports = function(core) {
285
299
  String.prototype.replace = patcher.unwrap(String.prototype.replace);
286
300
  }
287
301
  };
302
+
303
+ return propagator;
288
304
  };
@@ -36,6 +36,9 @@ module.exports = function (core) {
36
36
  patchType,
37
37
  post({ result: server, funcKey }) {
38
38
  server.addHook('preValidation', function preValidationHandler(request, reply, done) {
39
+ // todo(NODE-3793): support for @fastify/websocket
40
+ if (request.constructor.name == 'WebSocket') return;
41
+
39
42
  const sourceContext = getSourceContext();
40
43
  if (!sourceContext) return done();
41
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/assess",
3
- "version": "1.54.0",
3
+ "version": "1.55.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.46.0",
25
- "@contrast/core": "1.51.0",
26
- "@contrast/dep-hooks": "1.20.0",
24
+ "@contrast/config": "1.47.0",
25
+ "@contrast/core": "1.52.0",
26
+ "@contrast/dep-hooks": "1.21.0",
27
27
  "@contrast/distringuish": "^5.1.0",
28
- "@contrast/instrumentation": "1.30.0",
29
- "@contrast/logger": "1.24.0",
30
- "@contrast/patcher": "1.23.0",
31
- "@contrast/rewriter": "1.27.0",
32
- "@contrast/route-coverage": "1.42.0",
33
- "@contrast/scopes": "1.21.0",
28
+ "@contrast/instrumentation": "1.31.0",
29
+ "@contrast/logger": "1.25.0",
30
+ "@contrast/patcher": "1.24.0",
31
+ "@contrast/rewriter": "1.28.0",
32
+ "@contrast/route-coverage": "1.43.0",
33
+ "@contrast/scopes": "1.22.0",
34
34
  "semver": "^7.6.0"
35
35
  }
36
36
  }