@contrast/assess 1.54.0 → 1.54.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.
|
@@ -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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
174
|
-
|
|
181
|
+
// coerce the replacement to a string, e.g., null => 'null'
|
|
182
|
+
replacement = String(replacement);
|
|
175
183
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
198
|
+
const { _accumOffset, _accumTags } = data;
|
|
199
|
+
const { replacement, replacementTags } = propagator.getReplacementInfo(data, args, parsedArgs);
|
|
192
200
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/assess",
|
|
3
|
-
"version": "1.54.
|
|
3
|
+
"version": "1.54.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)",
|