@contrast/assess 1.46.3 → 1.48.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.
- package/lib/dataflow/propagation/install/contrast-methods/add.js +6 -1
- package/lib/dataflow/propagation/install/fastify-send.js +18 -2
- package/lib/dataflow/propagation/install/string/replace.js +127 -69
- package/lib/dataflow/propagation/install/string/replace.node-test.mjs +705 -0
- package/lib/dataflow/sources/install/hapi/hapi.js +4 -3
- package/lib/dataflow/tracker.js +5 -0
- package/lib/get-source-context.js +2 -3
- package/package.json +11 -11
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
+
const { isString } = require('@contrast/common');
|
|
18
19
|
const { createAppendTags } = require('../../../tag-utils');
|
|
19
20
|
|
|
20
21
|
module.exports = function(core) {
|
|
@@ -40,7 +41,11 @@ module.exports = function(core) {
|
|
|
40
41
|
// first get result, then following logic acts as post-hook in patcher speak
|
|
41
42
|
const result = add(...args);
|
|
42
43
|
|
|
43
|
-
if (
|
|
44
|
+
if (
|
|
45
|
+
!result ||
|
|
46
|
+
!isString(result) ||
|
|
47
|
+
!getPropagatorContext()
|
|
48
|
+
) return result;
|
|
44
49
|
|
|
45
50
|
const rInfo = tracker.getData(result);
|
|
46
51
|
if (rInfo) {
|
|
@@ -26,8 +26,8 @@ module.exports = function (core) {
|
|
|
26
26
|
|
|
27
27
|
return core.assess.dataflow.propagation.fastifySend = {
|
|
28
28
|
install() {
|
|
29
|
-
depHooks.resolve({ name: '@fastify/send', version: '<
|
|
30
|
-
patcher.patch(
|
|
29
|
+
depHooks.resolve({ name: '@fastify/send', version: '<3', file: 'lib/SendStream.js' }, (SendStream) => {
|
|
30
|
+
patcher.patch(SendStream.prototype, 'sendFile', {
|
|
31
31
|
name: '@fastify/send/lib/SendStream.js',
|
|
32
32
|
patchType,
|
|
33
33
|
usePerf: 'sync',
|
|
@@ -41,6 +41,22 @@ module.exports = function (core) {
|
|
|
41
41
|
},
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
|
+
|
|
45
|
+
depHooks.resolve({ name: '@fastify/send', version: '>=3 <5', file: 'lib/send.js' }, (send) => {
|
|
46
|
+
patcher.patch(send, 'send', {
|
|
47
|
+
name: '@fastify/send/lib/send.js',
|
|
48
|
+
patchType,
|
|
49
|
+
usePerf: 'sync',
|
|
50
|
+
pre(data) {
|
|
51
|
+
const { args } = data;
|
|
52
|
+
|
|
53
|
+
if (!getPropagatorContext()) return;
|
|
54
|
+
|
|
55
|
+
const untrackedPath = StringPrototypeSlice.call(` ${args[1]}`, 1);
|
|
56
|
+
args[1] = untrackedPath;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
});
|
|
44
60
|
}
|
|
45
61
|
};
|
|
46
62
|
};
|
|
@@ -18,11 +18,10 @@
|
|
|
18
18
|
const {
|
|
19
19
|
DataflowTag: { UNTRUSTED },
|
|
20
20
|
primordials: {
|
|
21
|
-
StringPrototypeMatch,
|
|
22
21
|
ArrayPrototypeJoin,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
StringPrototypeMatchAll,
|
|
23
|
+
StringPrototypeReplace,
|
|
24
|
+
StringPrototypeReplaceAll,
|
|
26
25
|
}
|
|
27
26
|
} = require('@contrast/common');
|
|
28
27
|
const {
|
|
@@ -43,98 +42,157 @@ module.exports = function(core) {
|
|
|
43
42
|
} = core;
|
|
44
43
|
|
|
45
44
|
function parseArgs(args) {
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return { match, captureGroups, matchIdx, str, namedGroups };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// these functions specifically use the patched versions of Substring so that
|
|
59
|
-
// the tags are propagated.
|
|
60
|
-
function replaceSpecialCharacters(replacement, { captureGroups, match, str, namedGroups }, replacementType) {
|
|
61
|
-
let ret = replacement;
|
|
62
|
-
[
|
|
63
|
-
{
|
|
64
|
-
regex: /\$\$/g,
|
|
65
|
-
replace: '$'
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
regex: /\$&/g,
|
|
69
|
-
replace: match
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
regex: /\$`/g,
|
|
73
|
-
replace: str.substring(0, str.indexOf(match))
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
regex: /\$'/g,
|
|
77
|
-
replace: str.substring(str.indexOf(match) + match.length, str.length)
|
|
78
|
-
}
|
|
79
|
-
].forEach(({ regex, replace }) => {
|
|
80
|
-
if (ret && StringPrototypeMatch.call(ret, regex)) {
|
|
81
|
-
// If the match string is tracked, we can actually use the patched replace
|
|
82
|
-
// to keep track of its tag ranges
|
|
83
|
-
if (tracker.getData(replace)) {
|
|
84
|
-
ret = ret.replace(regex, replace);
|
|
85
|
-
} else {
|
|
86
|
-
ret = StringPrototypeReplace.call(ret, regex, replace);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const numberedGroupMatches = replacementType !== 'function' && StringPrototypeMatch.call(replacement, /\$[1-9][0-9]|\$[1-9]/g);
|
|
92
|
-
if (numberedGroupMatches) {
|
|
93
|
-
numberedGroupMatches.forEach((numberedGroup) => {
|
|
94
|
-
const group = Number(StringPrototypeSubstring.call(numberedGroup, 1));
|
|
95
|
-
ret = StringPrototypeReplace.call(ret, numberedGroup, captureGroups[group - 1] || '');
|
|
96
|
-
});
|
|
45
|
+
//
|
|
46
|
+
// convert replace args (match, p1, p2, ..., matchIdx, str, ?groups) to
|
|
47
|
+
// re.exec(string) format [match, g1, g2, ..., index: N, input: '', groups?]
|
|
48
|
+
//
|
|
49
|
+
const r = [];
|
|
50
|
+
let ix = -1;
|
|
51
|
+
if (typeof args.at(-1) === 'object') {
|
|
52
|
+
r.groups = args.at(-1);
|
|
53
|
+
ix = -2;
|
|
97
54
|
}
|
|
55
|
+
r.input = args.at(ix);
|
|
56
|
+
ix -= 1;
|
|
57
|
+
r.index = args.at(ix);
|
|
98
58
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
59
|
+
// ix is negative from the end, so add it
|
|
60
|
+
ix = args.length + ix;
|
|
61
|
+
for (let i = 0; i < ix; i++) {
|
|
62
|
+
r.push(args[i]);
|
|
103
63
|
}
|
|
104
64
|
|
|
105
|
-
return
|
|
65
|
+
return r;
|
|
106
66
|
}
|
|
107
67
|
|
|
68
|
+
const RE_REPS = /\$(\$|&|`|'|[1-9][0-9]?|<[a-zA-Z0-9_]+>)/g;
|
|
69
|
+
const STR_REPS = /\$(\$|&|`|')/g;
|
|
70
|
+
|
|
108
71
|
function getReplacementInfo(data, replacerArgs, parsedArgs) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
72
|
+
// patternIsRE is two different flags, both booleans.
|
|
73
|
+
// - set true if the first argument to String.prototype.replace is an RE.
|
|
74
|
+
// - after the $N, $<name> checks, it flags that a substitution was made.
|
|
75
|
+
const patternIsRE = data.args[0] instanceof RegExp;
|
|
76
|
+
let replacement;
|
|
77
|
+
if (typeof data._replacement === 'function') {
|
|
78
|
+
// no special replacements apply to the string returned by a replacement
|
|
79
|
+
// function.
|
|
80
|
+
replacement = data._replacement.call(global, ...replacerArgs);
|
|
81
|
+
} else {
|
|
82
|
+
// if it's not a function then the valid special replacements depend on
|
|
83
|
+
// whether the pattern is a regex or a string. first find substitution
|
|
84
|
+
// patterns present in the replacement string. Don't find patterns that
|
|
85
|
+
// aren't valid, e.g., $0, $<name> when the pattern is a string.
|
|
86
|
+
replacement = String(data._replacement);
|
|
87
|
+
|
|
88
|
+
const matches = [...StringPrototypeMatchAll.call(replacement, patternIsRE ? RE_REPS : STR_REPS)];
|
|
89
|
+
let trackedReplacementsDone = 0;
|
|
112
90
|
|
|
113
|
-
|
|
91
|
+
for (const m of matches) {
|
|
92
|
+
let substitution;
|
|
93
|
+
let substitutionDone = false;
|
|
94
|
+
// format of m: ['$`', '`', index: 0, input: string, groups: undefined|{}]
|
|
95
|
+
// if the pattern is a regex, then $1 to $99 and $<name> are valid
|
|
96
|
+
if (patternIsRE) {
|
|
97
|
+
// my guess is $1 to $99 are most likely, after that, who knows? so
|
|
98
|
+
// we check named groups next, because 1) they seem more useful than
|
|
99
|
+
// the other $ patterns and 2) they are in the same patternIsRE test.
|
|
100
|
+
//
|
|
101
|
+
// in any case, the following will be false if m[1][0] is not a number.
|
|
102
|
+
if (m[1][0] >= 1 && m[1][0] <= 9) {
|
|
103
|
+
//const group = Number(m[1]);
|
|
104
|
+
// a group might not be present, e.g., (pattern)?(a) or (a)|(b).
|
|
105
|
+
if (parsedArgs[m[1]] === undefined && m[1] in parsedArgs) {
|
|
106
|
+
// need to remove $m[1] from replacement pattern. N.B. this could
|
|
107
|
+
// mess up if the replacement text contains text that matches a
|
|
108
|
+
// subsequent replacement (either RE or string).
|
|
109
|
+
replacement = StringPrototypeReplaceAll.call(replacement, m[0], '');
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
substitution = parsedArgs[m[1]];
|
|
113
|
+
substitutionDone = true;
|
|
114
|
+
} else if (m[1][0] === '<') {
|
|
115
|
+
// named group
|
|
116
|
+
const groupName = m[1].substring(1, m[1].length - 1);
|
|
117
|
+
if (parsedArgs.groups[groupName] === undefined && groupName in parsedArgs.groups) {
|
|
118
|
+
// remove $<groupName> from the replacement pattern. N.B. this
|
|
119
|
+
// also could mess up if the replacement text containts text that
|
|
120
|
+
// matches a subsequent replacement.
|
|
121
|
+
replacement = StringPrototypeReplaceAll.call(replacement, m[0], '');
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
substitution = parsedArgs.groups[groupName];
|
|
125
|
+
substitutionDone = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// the following are valid whether the pattern is a regex or a string.
|
|
130
|
+
//
|
|
131
|
+
// any idea what order $&'` should be in from a most-common to least-
|
|
132
|
+
// common perspective? nfi.
|
|
133
|
+
if (!substitutionDone) {
|
|
134
|
+
if (m[1] === '$') {
|
|
135
|
+
// this could actually be tracked but in order to do this "right"
|
|
136
|
+
// we have to be able to distinguish whether it is the first or
|
|
137
|
+
// the second $ that is tracked. so punt, and just ignore it, as
|
|
138
|
+
// originally implemented.
|
|
139
|
+
substitution = '$';
|
|
140
|
+
} else if (m[1] === '&') {
|
|
141
|
+
// replace these '$&' in parsedArgs[0] (replacerArgs[0], i.e., the
|
|
142
|
+
// match). if tracked, handle it. do so by index, so we don't have to
|
|
143
|
+
// call replace again? e.g., string[m.index..m.index+2]
|
|
144
|
+
substitution = parsedArgs[0];
|
|
145
|
+
} else if (m[1] === '`') {
|
|
146
|
+
substitution = parsedArgs.input.substring(0, parsedArgs.index);
|
|
147
|
+
} else if (m[1] === "'") {
|
|
148
|
+
substitution = parsedArgs.input.substring(parsedArgs.index + parsedArgs[0].length);
|
|
149
|
+
} // else {
|
|
150
|
+
// throw new Error('how can it have matched RE and gotten here?');
|
|
151
|
+
// }
|
|
152
|
+
// i'm not sure what the proper handling of this is. if either char
|
|
153
|
+
// of the $$&`' sequence is tracked, then something the user input
|
|
154
|
+
// is manipulating the output, even if it is just duplicating what
|
|
155
|
+
// might be tracked or untracked input. does that count?
|
|
156
|
+
}
|
|
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);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// coerce the replacement to a string, e.g., null => 'null'
|
|
169
|
+
replacement = String(replacement);
|
|
114
170
|
|
|
115
171
|
data._replacementInfo = tracker.getData(replacement);
|
|
116
172
|
if (data._replacementInfo) {
|
|
117
173
|
data._history.add(data._replacementInfo);
|
|
118
174
|
}
|
|
175
|
+
|
|
119
176
|
return { replacement, replacementInfo: data._replacementInfo };
|
|
120
177
|
}
|
|
121
178
|
|
|
122
179
|
function getReplacer(data) {
|
|
123
180
|
return function replacer(...args) {
|
|
124
181
|
const parsedArgs = parseArgs(args);
|
|
125
|
-
const
|
|
182
|
+
const match = parsedArgs[0];
|
|
183
|
+
const { index, input } = parsedArgs;
|
|
126
184
|
|
|
127
185
|
const { _accumOffset, _accumTags } = data;
|
|
128
186
|
const { replacement, replacementInfo } = getReplacementInfo(data, args, parsedArgs);
|
|
129
187
|
|
|
130
|
-
const preTags = createSubsetTags(_accumTags, 0, _accumOffset +
|
|
131
|
-
const postTags = createSubsetTags(_accumTags, _accumOffset +
|
|
188
|
+
const preTags = createSubsetTags(_accumTags, 0, _accumOffset + index);
|
|
189
|
+
const postTags = createSubsetTags(_accumTags, _accumOffset + index + match.length, input.length - index - match.length);
|
|
132
190
|
data._accumOffset += (replacement.length - match.length);
|
|
133
191
|
if (preTags || postTags || replacementInfo) {
|
|
134
192
|
data._accumTags = createAppendTags(
|
|
135
|
-
createAppendTags(preTags, replacementInfo?.tags, _accumOffset +
|
|
193
|
+
createAppendTags(preTags, replacementInfo?.tags, _accumOffset + index),
|
|
136
194
|
postTags,
|
|
137
|
-
data._accumOffset +
|
|
195
|
+
data._accumOffset + index + match.length
|
|
138
196
|
);
|
|
139
197
|
} else {
|
|
140
198
|
data._accumTags = {};
|
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { inspect } from 'util';
|
|
4
|
+
import { initAssessFixture } from '@contrast/test/fixtures/index.js';
|
|
5
|
+
|
|
6
|
+
describe('assess dataflow propagation string replace', function () {
|
|
7
|
+
let core, simulateRequestScope, trackString, tracker;
|
|
8
|
+
const allPatcher = new Map();
|
|
9
|
+
|
|
10
|
+
beforeEach(function () {
|
|
11
|
+
({
|
|
12
|
+
core,
|
|
13
|
+
trackString,
|
|
14
|
+
simulateRequestScope
|
|
15
|
+
} = initAssessFixture());
|
|
16
|
+
|
|
17
|
+
// print test name
|
|
18
|
+
//console.log(`beforeEach TestContext(${this.name})`);
|
|
19
|
+
|
|
20
|
+
tracker = core.assess.dataflow.tracker;
|
|
21
|
+
|
|
22
|
+
core.assess.dataflow.propagation.stringInstrumentation.substring.install();
|
|
23
|
+
core.assess.dataflow.propagation.stringInstrumentation.replace.install();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(function () {
|
|
27
|
+
core.assess.dataflow.propagation.stringInstrumentation.replace.uninstall();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// eslint-disable-next-line mocha/no-sibling-hooks
|
|
31
|
+
afterEach(function() {
|
|
32
|
+
core.Perf.fromAllToMap('patcher', allPatcher);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
after(function() {
|
|
36
|
+
const stats = core.Perf.getStats(allPatcher);
|
|
37
|
+
for (const [key, { n, totalMicros, mean }] of stats.entries()) {
|
|
38
|
+
console.log(key, n, totalMicros, 'nsec', mean, 'mean');
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
['string', 'function'].forEach((replacerType) => {
|
|
43
|
+
function getReplacement(replacerType, replacement) {
|
|
44
|
+
return replacerType === 'function' ? () => replacement : replacement;
|
|
45
|
+
}
|
|
46
|
+
describe(`string-arg custom ${replacerType} replacer`, function () {
|
|
47
|
+
it('untracked string replace with untracked string', function () {
|
|
48
|
+
simulateRequestScope(() => {
|
|
49
|
+
const val = '?bcd';
|
|
50
|
+
const ret = val.replace('?', getReplacement(replacerType, 'a'));
|
|
51
|
+
assert.strictEqual(ret, 'abcd');
|
|
52
|
+
assert.strictEqual(tracker.getData(ret), null);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
[
|
|
57
|
+
[],
|
|
58
|
+
[undefined],
|
|
59
|
+
[null]
|
|
60
|
+
].forEach((rest) => {
|
|
61
|
+
it(`full replacement with nullish value: ${inspect(rest[0])}`, function () {
|
|
62
|
+
simulateRequestScope(() => {
|
|
63
|
+
const val = 'abcd';
|
|
64
|
+
const extern = trackString(val);
|
|
65
|
+
const ret = extern.replace(val, getReplacement(replacerType, rest[0]));
|
|
66
|
+
assert.strictEqual(ret, String(rest[0]));
|
|
67
|
+
assert.strictEqual(tracker.getData(ret), null);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('empty match string', function () {
|
|
73
|
+
simulateRequestScope(() => {
|
|
74
|
+
const extern = trackString('bcde', {
|
|
75
|
+
tags: {
|
|
76
|
+
UNTRUSTED: [0, 3]
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
const ret = extern.replace('', getReplacement(replacerType, 'a'));
|
|
80
|
+
assert.strictEqual(ret, 'abcde');
|
|
81
|
+
const tags = tracker.getData(ret).tags;
|
|
82
|
+
const expectedTags = { UNTRUSTED: [1, 4] };
|
|
83
|
+
assertTagsEqual(tags, expectedTags);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('empty replacer string', function () {
|
|
88
|
+
simulateRequestScope(() => {
|
|
89
|
+
const extern = trackString('abcd', {
|
|
90
|
+
tags: {
|
|
91
|
+
UNTRUSTED: [0, 3]
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const ret = extern.replace('', getReplacement(replacerType, ''));
|
|
95
|
+
assert.strictEqual(ret, 'abcd');
|
|
96
|
+
const tags = tracker.getData(ret).tags;
|
|
97
|
+
const expectedTags = { UNTRUSTED: [0, 3] };
|
|
98
|
+
assertTagsEqual(tags, expectedTags);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('untracked string replaced with tracked', function () {
|
|
103
|
+
simulateRequestScope(() => {
|
|
104
|
+
const val = '?bcd';
|
|
105
|
+
const replacement = trackString('a', {
|
|
106
|
+
tags: {
|
|
107
|
+
UNTRUSTED: [0, 0]
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
const ret = val.replace('?', getReplacement(replacerType, replacement));
|
|
111
|
+
assert.strictEqual(ret, 'abcd');
|
|
112
|
+
const tags = tracker.getData(ret).tags;
|
|
113
|
+
const expectedTags = { UNTRUSTED: [0, 0] };
|
|
114
|
+
assertTagsEqual(tags, expectedTags);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('does not track return with all tracked values removed', function () {
|
|
119
|
+
simulateRequestScope(() => {
|
|
120
|
+
const extern = trackString('_TT_', {
|
|
121
|
+
tags: {
|
|
122
|
+
UNTRUSTED: [1, 2]
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const ret = extern.replace('TT', getReplacement(replacerType, '__'));
|
|
126
|
+
assert.strictEqual(ret, '____');
|
|
127
|
+
assert.strictEqual(tracker.getData(ret), null);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('tracks result highspan', function () {
|
|
132
|
+
simulateRequestScope(() => {
|
|
133
|
+
const extern = trackString('_TT_', {
|
|
134
|
+
tags: {
|
|
135
|
+
UNTRUSTED: [1, 2],
|
|
136
|
+
history: [{ mock: 'sourceEvent' }]
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
const ret = extern.replace('_T', getReplacement(replacerType, '__'));
|
|
140
|
+
assert.strictEqual(ret, '__T_');
|
|
141
|
+
const tags = tracker.getData(ret).tags;
|
|
142
|
+
const expectedTags = { UNTRUSTED: [2, 2] };
|
|
143
|
+
assertTagsEqual(tags, expectedTags);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('tracks result lospan', function () {
|
|
148
|
+
simulateRequestScope(() => {
|
|
149
|
+
const extern = trackString('_TT_', {
|
|
150
|
+
tags: {
|
|
151
|
+
UNTRUSTED: [1, 2],
|
|
152
|
+
history: [{ mock: 'sourceEvent' }]
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const ret = extern.replace('T_', getReplacement(replacerType, '__'));
|
|
156
|
+
assert.strictEqual(ret, '_T__');
|
|
157
|
+
const tags = tracker.getData(ret).tags;
|
|
158
|
+
const expectedTags = { UNTRUSTED: [1, 1] };
|
|
159
|
+
assertTagsEqual(tags, expectedTags);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('tracks result highspan tracked replacement', function () {
|
|
164
|
+
simulateRequestScope(() => {
|
|
165
|
+
const extern = trackString('_TT_', {
|
|
166
|
+
tags: {
|
|
167
|
+
UNTRUSTED: [1, 2]
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
const replacement = trackString('TT', {
|
|
171
|
+
tags: {
|
|
172
|
+
UNTRUSTED: [0, 1]
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
const ret = extern.replace('T_', getReplacement(replacerType, replacement));
|
|
176
|
+
assert.strictEqual(ret, '_TTT');
|
|
177
|
+
const tags = tracker.getData(ret).tags;
|
|
178
|
+
const expectedTags = { UNTRUSTED: [1, 3] };
|
|
179
|
+
assertTagsEqual(tags, expectedTags);
|
|
180
|
+
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('keeps track of multiple tag ranges', function () {
|
|
185
|
+
simulateRequestScope(() => {
|
|
186
|
+
const extern = trackString('a?cd', {
|
|
187
|
+
tags: {
|
|
188
|
+
UNTRUSTED: [0, 3],
|
|
189
|
+
foo: [1, 2]
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
const replacement = trackString('b', {
|
|
193
|
+
tags: {
|
|
194
|
+
foo: [0, 0],
|
|
195
|
+
bar: [0, 0]
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
const ret = extern.replace('?', getReplacement(replacerType, replacement));
|
|
199
|
+
assert.strictEqual(ret, 'abcd');
|
|
200
|
+
const tags = tracker.getData(ret).tags;
|
|
201
|
+
assertTagsEqual(tags, {
|
|
202
|
+
UNTRUSTED: [0, 0, 2, 3],
|
|
203
|
+
foo: [1, 2],
|
|
204
|
+
bar: [1, 1]
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe(`regEx custom ${replacerType} replacer`, function () {
|
|
211
|
+
it('keeps track of tagged strings not replaced', function () {
|
|
212
|
+
simulateRequestScope(() => {
|
|
213
|
+
const extern = trackString('abcd');
|
|
214
|
+
const ret = extern.replace(/e/g, getReplacement(replacerType, 'e'));
|
|
215
|
+
assert.strictEqual(ret, 'abcd');
|
|
216
|
+
const tags = tracker.getData(ret).tags;
|
|
217
|
+
assertTagsEqual(tags, {
|
|
218
|
+
UNTRUSTED: [0, 3]
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('keeps track of tagged strings replaced by untracked strings', function () {
|
|
224
|
+
simulateRequestScope(() => {
|
|
225
|
+
const extern = trackString('a?a?');
|
|
226
|
+
const ret = extern.replace(/\?/g, getReplacement(replacerType, 'b'));
|
|
227
|
+
assert.strictEqual(ret, 'abab');
|
|
228
|
+
const tags = tracker.getData(ret).tags;
|
|
229
|
+
const expectedTags = {
|
|
230
|
+
UNTRUSTED: [0, 0, 2, 2]
|
|
231
|
+
};
|
|
232
|
+
assertTagsEqual(tags, expectedTags);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('keeps track of tagged strings replaced by larger untracked strings', function () {
|
|
237
|
+
simulateRequestScope(() => {
|
|
238
|
+
const extern = trackString('a?a?');
|
|
239
|
+
const ret = extern.replace(/\?/g, getReplacement(replacerType, 'bbb'));
|
|
240
|
+
assert.strictEqual(ret, 'abbbabbb');
|
|
241
|
+
const tags = tracker.getData(ret).tags;
|
|
242
|
+
const expectedTags = {
|
|
243
|
+
UNTRUSTED: [0, 0, 4, 4]
|
|
244
|
+
};
|
|
245
|
+
assertTagsEqual(tags, expectedTags);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('keeps track of tagged strings replaced by smaller untracked strings', function () {
|
|
250
|
+
simulateRequestScope(() => {
|
|
251
|
+
const extern = trackString('a???a???');
|
|
252
|
+
const ret = extern.replace(/\?\?\?/g, getReplacement(replacerType, 'b'));
|
|
253
|
+
assert.strictEqual(ret, 'abab');
|
|
254
|
+
const tags = tracker.getData(ret).tags;
|
|
255
|
+
const expectedTags = {
|
|
256
|
+
UNTRUSTED: [0, 0, 2, 2]
|
|
257
|
+
};
|
|
258
|
+
assertTagsEqual(tags, expectedTags);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('keeps track of tagged strings replaced by tracked strings', function () {
|
|
263
|
+
simulateRequestScope(() => {
|
|
264
|
+
const extern = trackString('a?a?', {
|
|
265
|
+
tags: {
|
|
266
|
+
UNTRUSTED: [0, 3]
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
const replacement = trackString('b', {
|
|
270
|
+
tags: {
|
|
271
|
+
UNTRUSTED: [0, 0]
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
const ret = extern.replace(/\?/g, getReplacement(replacerType, replacement));
|
|
275
|
+
assert.strictEqual(ret, 'abab');
|
|
276
|
+
const tags = tracker.getData(ret).tags;
|
|
277
|
+
const expectedTags = {
|
|
278
|
+
UNTRUSTED: [0, 3]
|
|
279
|
+
};
|
|
280
|
+
assertTagsEqual(tags, expectedTags);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('keeps track of tagged strings replaced by larger tracked strings', function () {
|
|
285
|
+
simulateRequestScope(() => {
|
|
286
|
+
const extern = trackString('a?a?', {
|
|
287
|
+
tags: {
|
|
288
|
+
UNTRUSTED: [0, 3]
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
const replacement = trackString('bbb', {
|
|
292
|
+
tags: {
|
|
293
|
+
UNTRUSTED: [0, 2]
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
const ret = extern.replace(/\?/g, getReplacement(replacerType, replacement));
|
|
297
|
+
assert.strictEqual(ret, 'abbbabbb');
|
|
298
|
+
const tags = tracker.getData(ret).tags;
|
|
299
|
+
const expectedTags = {
|
|
300
|
+
UNTRUSTED: [0, 7]
|
|
301
|
+
};
|
|
302
|
+
assertTagsEqual(tags, expectedTags);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('keeps track of tagged strings replaced by smaller tracked strings', function () {
|
|
307
|
+
simulateRequestScope(() => {
|
|
308
|
+
const extern = trackString('a???a???', {
|
|
309
|
+
tags: {
|
|
310
|
+
UNTRUSTED: [0, 7]
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
const replacement = trackString('b', {
|
|
314
|
+
tags: {
|
|
315
|
+
UNTRUSTED: [0, 0]
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
const ret = extern.replace(/\?\?\?/g, getReplacement(replacerType, replacement));
|
|
319
|
+
assert.strictEqual(ret, 'abab');
|
|
320
|
+
const tags = tracker.getData(ret).tags;
|
|
321
|
+
const expectedTags = {
|
|
322
|
+
UNTRUSTED: [0, 3]
|
|
323
|
+
};
|
|
324
|
+
assertTagsEqual(tags, expectedTags);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('keeps track of multiple tag ranges', function () {
|
|
329
|
+
simulateRequestScope(() => {
|
|
330
|
+
const extern = trackString('a??a??a', {
|
|
331
|
+
tags: {
|
|
332
|
+
UNTRUSTED: [0, 6],
|
|
333
|
+
foo: [0, 2],
|
|
334
|
+
bar: [4, 6]
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
const replacement = trackString('b', {
|
|
338
|
+
tags: {
|
|
339
|
+
foo: [0, 0],
|
|
340
|
+
bar: [0, 0]
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
const ret = extern.replace(/\?/g, replacement);
|
|
344
|
+
assert.strictEqual(ret, 'abbabba');
|
|
345
|
+
const tags = tracker.getData(ret).tags;
|
|
346
|
+
const expectedTags = {
|
|
347
|
+
UNTRUSTED: [0, 0, 3, 3, 6, 6],
|
|
348
|
+
foo: [0, 2, 4, 5],
|
|
349
|
+
bar: [1, 2, 4, 6],
|
|
350
|
+
};
|
|
351
|
+
assertTagsEqual(tags, expectedTags);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe('complex/edge cases', function () {
|
|
358
|
+
it('replacer function returns a non-string', function () {
|
|
359
|
+
simulateRequestScope(() => {
|
|
360
|
+
const extern = trackString('123?', {
|
|
361
|
+
tags: {
|
|
362
|
+
UNTRUSTED: [0, 3]
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
const ret = extern.replace('?', (match, offset) => offset + 1);
|
|
366
|
+
assert.strictEqual(ret, '1234');
|
|
367
|
+
const tags = tracker.getData(ret).tags;
|
|
368
|
+
assertTagsEqual(tags, {
|
|
369
|
+
UNTRUSTED: [0, 2]
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('replacer operates on capture groups', function () {
|
|
375
|
+
simulateRequestScope(() => {
|
|
376
|
+
const extern = trackString('AaaBbbCcc', {
|
|
377
|
+
tags: {
|
|
378
|
+
UNTRUSTED: [0, 8],
|
|
379
|
+
groupOne: [0, 2],
|
|
380
|
+
groupTwo: [3, 5],
|
|
381
|
+
groupThree: [6, 8]
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
const ret = extern.replace(/(A)|(B)|(C)/g, (match => match.toLowerCase()));
|
|
385
|
+
assert.strictEqual(ret, 'aaabbbccc');
|
|
386
|
+
const tags = tracker.getData(ret).tags;
|
|
387
|
+
assertTagsEqual(tags, {
|
|
388
|
+
UNTRUSTED: [1, 2, 4, 5, 7, 8],
|
|
389
|
+
groupOne: [1, 2],
|
|
390
|
+
groupTwo: [4, 5],
|
|
391
|
+
groupThree: [7, 8]
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('handles edge case where we have a function that sets the replacement to be a special value',
|
|
397
|
+
function () {
|
|
398
|
+
simulateRequestScope(() => {
|
|
399
|
+
let counter = 0;
|
|
400
|
+
const extern = trackString('SELECT ? FROM ?', {
|
|
401
|
+
tags: {
|
|
402
|
+
UNTRUSTED: [0, 14],
|
|
403
|
+
groupOne: [9, 12],
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
const ret = extern.replace(/(\?)/g, () => `$${counter++}`);
|
|
407
|
+
assert.strictEqual(ret, 'SELECT $0 FROM $1');
|
|
408
|
+
const tags = tracker.getData(ret).tags;
|
|
409
|
+
assertTagsEqual(tags, {
|
|
410
|
+
UNTRUSTED: [0, 6, 9, 14],
|
|
411
|
+
groupOne: [10, 13],
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('replacer operates on named capture groups', function () {
|
|
417
|
+
simulateRequestScope(() => {
|
|
418
|
+
const extern = trackString('AaaBbbCcc', {
|
|
419
|
+
tags: {
|
|
420
|
+
UNTRUSTED: [0, 8],
|
|
421
|
+
groupOne: [0, 2],
|
|
422
|
+
groupTwo: [3, 5],
|
|
423
|
+
groupThree: [6, 8]
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
const ret = extern.replace(/(?<g1>A)|(?<g2>B)|(?<g3>C)/g, (match => match.toLowerCase()));
|
|
427
|
+
assert.strictEqual(ret, 'aaabbbccc');
|
|
428
|
+
const tags = tracker.getData(ret).tags;
|
|
429
|
+
assertTagsEqual(tags, {
|
|
430
|
+
UNTRUSTED: [1, 2, 4, 5, 7, 8],
|
|
431
|
+
groupOne: [1, 2],
|
|
432
|
+
groupTwo: [4, 5],
|
|
433
|
+
groupThree: [7, 8]
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('keeps track of multiple overlapping tag ranges over multiple calls', function () {
|
|
439
|
+
simulateRequestScope(() => {
|
|
440
|
+
let ret;
|
|
441
|
+
const extern = trackString('a??cd?g?', {
|
|
442
|
+
tags: {
|
|
443
|
+
UNTRUSTED: [0, 7],
|
|
444
|
+
foo: [0, 2],
|
|
445
|
+
bar: [1, 3],
|
|
446
|
+
fizz: [4, 7],
|
|
447
|
+
buzz: [1, 6]
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
const replB = trackString('b', {
|
|
451
|
+
tags: {
|
|
452
|
+
foo: [0, 0],
|
|
453
|
+
bar: [0, 0]
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
const replEf = trackString('ef', {
|
|
457
|
+
tags: {
|
|
458
|
+
fizz: [0, 0],
|
|
459
|
+
buzz: [1, 1]
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
const replH = trackString('h', {
|
|
463
|
+
tags: {
|
|
464
|
+
foobar: [0, 0]
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
ret = extern.replace('??', replB);
|
|
468
|
+
ret = ret.replace('?', () => replEf);
|
|
469
|
+
ret = ret.replace(/\?/g, replH);
|
|
470
|
+
assert.strictEqual(ret, 'abcdefgh');
|
|
471
|
+
const tags = tracker.getData(ret).tags;
|
|
472
|
+
assertTagsEqual(tags, {
|
|
473
|
+
UNTRUSTED: [0, 0, 2, 3, 6, 6],
|
|
474
|
+
foo: [0, 1],
|
|
475
|
+
bar: [1, 2],
|
|
476
|
+
fizz: [3, 4, 6, 6],
|
|
477
|
+
buzz: [2, 3, 5, 6],
|
|
478
|
+
foobar: [7, 7]
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('keeps track of replacement history', function () {
|
|
484
|
+
simulateRequestScope(() => {
|
|
485
|
+
const extern = trackString('aa??');
|
|
486
|
+
const replacementOne = trackString('bb');
|
|
487
|
+
const replacementTwo = trackString('cc');
|
|
488
|
+
const replacements = ['', '', replacementOne, replacementTwo];
|
|
489
|
+
const ret = extern.replace(/\?/g, (match, offset) => replacements[offset]);
|
|
490
|
+
assert.strictEqual(ret, 'aabbcc');
|
|
491
|
+
const history = tracker.getData(ret).history;
|
|
492
|
+
assert.strictEqual(history.length, 3);
|
|
493
|
+
const expected = [
|
|
494
|
+
{ value: 'aa??', tags: { UNTRUSTED: [0, 3] } },
|
|
495
|
+
{ value: 'bb', tags: { UNTRUSTED: [0, 1] } },
|
|
496
|
+
{ value: 'cc', tags: { UNTRUSTED: [0, 1] } }
|
|
497
|
+
];
|
|
498
|
+
for (const {value, tags: expectedTags} of expected) {
|
|
499
|
+
const {tags} = history.find(({value: v}) => v === value);
|
|
500
|
+
assertTagsEqual(tags, expectedTags);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
describe('special characters', function () {
|
|
507
|
+
|
|
508
|
+
it('replaces and handles $$', function () {
|
|
509
|
+
simulateRequestScope(() => {
|
|
510
|
+
const extern = trackString('foo?foo');
|
|
511
|
+
const ret = extern.replace('?', '$$ $$');
|
|
512
|
+
assert.strictEqual(ret, 'foo$ $foo');
|
|
513
|
+
const tags = tracker.getData(ret).tags;
|
|
514
|
+
const expectedTags = { UNTRUSTED: [0, 2, 6, 8] };
|
|
515
|
+
assertTagsEqual(tags, expectedTags);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('BUG: does not replace $$ from a function replacer', function() {
|
|
520
|
+
// BUG
|
|
521
|
+
// $$ should no be interpreted when returned from a function
|
|
522
|
+
//
|
|
523
|
+
// NEED TEST when '$$' replacement is tracked?
|
|
524
|
+
simulateRequestScope(() => {
|
|
525
|
+
const extern = trackString('foo?foo');
|
|
526
|
+
const ret = extern.replace('?', () => '$$');
|
|
527
|
+
assert.strictEqual(ret, 'foo$$foo');
|
|
528
|
+
const tags = tracker.getData(ret).tags;
|
|
529
|
+
const expectedTags = { UNTRUSTED: [0, 2, 5, 7] };
|
|
530
|
+
assertTagsEqual(tags, expectedTags);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('replaces and handles untracked $&', function () {
|
|
535
|
+
simulateRequestScope(() => {
|
|
536
|
+
const extern = trackString('foo bar');
|
|
537
|
+
const ret = extern.replace('bar', '"$&"-[$&]');
|
|
538
|
+
assert.strictEqual(ret, 'foo "bar"-[bar]');
|
|
539
|
+
const tags = tracker.getData(ret).tags;
|
|
540
|
+
const expectedTags = { UNTRUSTED: [0, 3] };
|
|
541
|
+
assertTagsEqual(tags, expectedTags);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('replaces and handles tracked $&', function () {
|
|
546
|
+
simulateRequestScope(() => {
|
|
547
|
+
const extern = trackString('foo bar');
|
|
548
|
+
const replacement = trackString('bar');
|
|
549
|
+
const ret = extern.replace(replacement, '[$&]-[$&]');
|
|
550
|
+
assert.strictEqual(ret, 'foo [bar]-[bar]');
|
|
551
|
+
const tags = tracker.getData(ret).tags;
|
|
552
|
+
const expectedTags = { UNTRUSTED: [0, 3, 5, 7, 11, 13] };
|
|
553
|
+
assertTagsEqual(tags, expectedTags);
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('replaces and handles $`', function () {
|
|
558
|
+
simulateRequestScope(() => {
|
|
559
|
+
const extern = trackString('foobar');
|
|
560
|
+
const ret = extern.replace('bar', '$`$`');
|
|
561
|
+
assert.strictEqual(ret, 'foofoofoo');
|
|
562
|
+
const tags = tracker.getData(ret).tags;
|
|
563
|
+
const expectedTags = { UNTRUSTED: [0, 8] };
|
|
564
|
+
assertTagsEqual(tags, expectedTags);
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('replaces and handles $\'', function () {
|
|
569
|
+
simulateRequestScope(() => {
|
|
570
|
+
const extern = trackString('foobar');
|
|
571
|
+
const ret = extern.replace('foo', '$\'$\'');
|
|
572
|
+
assert.strictEqual(ret, 'barbarbar');
|
|
573
|
+
const tags = tracker.getData(ret).tags;
|
|
574
|
+
const expectedTags = { UNTRUSTED: [0, 8] };
|
|
575
|
+
assertTagsEqual(tags, expectedTags);
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('replaces and handles $N', function () {
|
|
580
|
+
simulateRequestScope(() => {
|
|
581
|
+
const extern = trackString('aa bbcc');
|
|
582
|
+
const ret = extern.replace(/(aa) (bb)/g, '$2 $1');
|
|
583
|
+
assert.strictEqual(ret, 'bb aacc');
|
|
584
|
+
const { tags } = tracker.getData(ret);
|
|
585
|
+
// BUG? the entire input string is tracked. replace() reorganizes it,
|
|
586
|
+
// but shouldn't the entire resultant string be tracked? maybe just
|
|
587
|
+
// handle the simplest case where the entire input string is tracked
|
|
588
|
+
// because if only subsets of the string are tracked it will be quite
|
|
589
|
+
// expensive to propagate the tag ranges correctly.
|
|
590
|
+
const expectedTags = { UNTRUSTED: [5, 6] };
|
|
591
|
+
assertTagsEqual(tags, expectedTags);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('BUG: replaces and handles $name', function () {
|
|
596
|
+
// BUG string needs to be $<name> not $name
|
|
597
|
+
simulateRequestScope(() => {
|
|
598
|
+
const extern = trackString('aabbcc');
|
|
599
|
+
const ret = extern.replace(/(?<g1>aa)(?<g2>bb)/g, '$<g2>$<g1>');
|
|
600
|
+
assert.strictEqual(ret, 'bbaacc');
|
|
601
|
+
const tags = tracker.getData(ret).tags;
|
|
602
|
+
const expectedTags = { UNTRUSTED: [4, 5] };
|
|
603
|
+
assertTagsEqual(tags, expectedTags);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('maintains tracking with $$', function() {
|
|
608
|
+
simulateRequestScope(() => {
|
|
609
|
+
const extern = trackString('foobarfoo');
|
|
610
|
+
const ret = extern.replace('bar', '$$');
|
|
611
|
+
assert.strictEqual(ret, 'foo$foo');
|
|
612
|
+
const tags = tracker.getData(ret).tags;
|
|
613
|
+
const expectedTags = { UNTRUSTED: [0, 2, 4, 6] };
|
|
614
|
+
assertTagsEqual(tags, expectedTags);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('BUG: maintains tracking when $$ is tracked', function() {
|
|
619
|
+
// BUG this should be [0, 6] but isn't because we don't consider whether
|
|
620
|
+
// the $$ is tracked or not.
|
|
621
|
+
simulateRequestScope(() => {
|
|
622
|
+
const dollardollar = trackString('$$');
|
|
623
|
+
const extern = trackString('foobarfoo');
|
|
624
|
+
const ret = extern.replace('bar', dollardollar);
|
|
625
|
+
assert.strictEqual(ret, 'foo$foo');
|
|
626
|
+
const tags = tracker.getData(ret).tags;
|
|
627
|
+
const expectedTags = { UNTRUSTED: [0, 2, 4, 6] };
|
|
628
|
+
assertTagsEqual(tags, expectedTags);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('replaces and handles strings containing multiple special characters', function () {
|
|
633
|
+
simulateRequestScope(() => {
|
|
634
|
+
const extern = trackString('aabbcc');
|
|
635
|
+
const ret = extern.replace('bb', '($&)-($\')-($`)');
|
|
636
|
+
assert.strictEqual(ret, 'aa(bb)-(cc)-(aa)cc');
|
|
637
|
+
const tags = tracker.getData(ret).tags;
|
|
638
|
+
const expectedTags = { UNTRUSTED: [0, 1, 8, 9, 13, 14, 16, 17] };
|
|
639
|
+
assertTagsEqual(tags, expectedTags);
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('replaces and handles strings containing multiple special characters that replace multiple strings', function () {
|
|
644
|
+
simulateRequestScope(() => {
|
|
645
|
+
const extern = trackString('aa?bb?cc?');
|
|
646
|
+
const ret = extern.replace(/(\?)/g, '$$$1');
|
|
647
|
+
assert.strictEqual(ret, 'aa$?bb$?cc$?');
|
|
648
|
+
const tags = tracker.getData(ret).tags;
|
|
649
|
+
const expectedTags = { UNTRUSTED: [0, 1, 4, 5, 8, 9 ]};
|
|
650
|
+
assertTagsEqual(tags, expectedTags);
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('replaces and handles multiple special characters and tracked strings', function () {
|
|
655
|
+
simulateRequestScope(() => {
|
|
656
|
+
const extern = trackString('foobar');
|
|
657
|
+
const replacement = trackString('bar');
|
|
658
|
+
const ret = extern.replace(replacement, '$$$&$$$&');
|
|
659
|
+
assert.strictEqual(ret, 'foo$bar$bar');
|
|
660
|
+
const tags = tracker.getData(ret).tags;
|
|
661
|
+
const expectedTags = {UNTRUSTED: [0, 2, 4, 6, 8, 10]};
|
|
662
|
+
assertTagsEqual(tags, expectedTags);
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// example is from shell-quote
|
|
667
|
+
it('replaces and handles $\\d replacements when capture group exists or doesn\'t', function() {
|
|
668
|
+
const s = 'http://example.com?a=1&a=2';
|
|
669
|
+
const expected = s.replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, '$1\\$2');
|
|
670
|
+
simulateRequestScope(() => {
|
|
671
|
+
const result = s.replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, '$1\\$2');
|
|
672
|
+
// http\://example.com\?a\=1\&a\=2
|
|
673
|
+
assert.strictEqual(result, expected);
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('handles disjunction (unmatched but present group)', function() {
|
|
678
|
+
const s = 'http://example.com?a=1&a=2';
|
|
679
|
+
const expected = s.replace(/(http)|(https)/g, '$2');
|
|
680
|
+
simulateRequestScope(() => {
|
|
681
|
+
const result = s.replace(/(http)|(https)/g, '$2');
|
|
682
|
+
// http\://example.com\?a\=1\&a\=2
|
|
683
|
+
assert.strictEqual(result, expected);
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('replaces and handles $<name> replacements when capture group exists or doesn\'t', function() {
|
|
688
|
+
const s = 'http://example.com?a=1&a=2';
|
|
689
|
+
const expected = s.replace(/(?<protocol>[A-Za-z]:)?(?<special>[#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, '$<protocol>\\$<special>');
|
|
690
|
+
simulateRequestScope(() => {
|
|
691
|
+
const result = s.replace(/(?<protocol>[A-Za-z]:)?(?<special>[#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, '$<protocol>\\$<special>');
|
|
692
|
+
// http\://example.com\?a\=1\&a\=2
|
|
693
|
+
assert.strictEqual(result, expected);
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
function assertTagsEqual(tags, expectedTags) {
|
|
701
|
+
assert.deepStrictEqual(Object.keys(tags), Object.keys(expectedTags), 'tag mismatch');
|
|
702
|
+
for (const tag of Object.keys(tags)) {
|
|
703
|
+
assert.deepStrictEqual(tags[tag], expectedTags[tag], "tag range mismatch");
|
|
704
|
+
}
|
|
705
|
+
}
|
|
@@ -31,7 +31,7 @@ module.exports = function (core) {
|
|
|
31
31
|
|
|
32
32
|
const source = sources.hapiInstrumentation.hapi = {
|
|
33
33
|
install() {
|
|
34
|
-
|
|
34
|
+
depHooks.resolve({ name: '@hapi/hapi', version: '>=18 <22' }, (hapi) => {
|
|
35
35
|
['server', 'Server'].forEach((server) => {
|
|
36
36
|
patcher.patch(hapi, server, {
|
|
37
37
|
name: `hapi.${server}`,
|
|
@@ -40,7 +40,7 @@ module.exports = function (core) {
|
|
|
40
40
|
|
|
41
41
|
server.ext('onRequest', (req, h) => {
|
|
42
42
|
const sourceContext = getSourceContext();
|
|
43
|
-
if (!sourceContext) return;
|
|
43
|
+
if (!sourceContext) return h.continue;
|
|
44
44
|
|
|
45
45
|
[
|
|
46
46
|
{ key: 'query', inputType: InputType.QUERYSTRING, trackedFlag: 'parsedQuery' },
|
|
@@ -70,9 +70,10 @@ module.exports = function (core) {
|
|
|
70
70
|
});
|
|
71
71
|
return h.continue;
|
|
72
72
|
});
|
|
73
|
+
|
|
73
74
|
server.ext('onPostAuth', (req, h) => {
|
|
74
75
|
const sourceContext = core.scopes.sources.getStore()?.assess;
|
|
75
|
-
if (!sourceContext) return;
|
|
76
|
+
if (!sourceContext) return h.continue;
|
|
76
77
|
|
|
77
78
|
[
|
|
78
79
|
{ key: 'state', inputType: InputType.COOKIE_VALUE, trackedFlag: 'parsedCookies' },
|
package/lib/dataflow/tracker.js
CHANGED
|
@@ -37,6 +37,10 @@ module.exports = function tracker(core) {
|
|
|
37
37
|
return objMap.get(value) || null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function isTracked(value) {
|
|
41
|
+
return distringuish.isExternal(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
40
44
|
function track(value, metadata) {
|
|
41
45
|
let ret = Object.create(null);
|
|
42
46
|
|
|
@@ -148,5 +152,6 @@ module.exports = function tracker(core) {
|
|
|
148
152
|
untrack,
|
|
149
153
|
getData,
|
|
150
154
|
getInfo: getData,
|
|
155
|
+
isTracked,
|
|
151
156
|
};
|
|
152
157
|
};
|
|
@@ -61,7 +61,7 @@ module.exports = function(core) {
|
|
|
61
61
|
return null;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
if (ctx.propagationEventsCount
|
|
64
|
+
if (ctx.propagationEventsCount >= config.assess.max_propagation_events) return null;
|
|
65
65
|
|
|
66
66
|
return ctx;
|
|
67
67
|
};
|
|
@@ -105,9 +105,8 @@ module.exports = function(core) {
|
|
|
105
105
|
return null;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
if (ctx.sourceEventsCount
|
|
108
|
+
if (ctx.sourceEventsCount >= config.assess.max_context_source_events) return null;
|
|
109
109
|
|
|
110
110
|
return ctx;
|
|
111
111
|
};
|
|
112
|
-
|
|
113
112
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/assess",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.48.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)",
|
|
@@ -20,17 +20,17 @@
|
|
|
20
20
|
"test": "../scripts/test.sh"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@contrast/common": "1.
|
|
24
|
-
"@contrast/config": "1.
|
|
25
|
-
"@contrast/core": "1.
|
|
26
|
-
"@contrast/dep-hooks": "1.
|
|
23
|
+
"@contrast/common": "1.30.0",
|
|
24
|
+
"@contrast/config": "1.41.0",
|
|
25
|
+
"@contrast/core": "1.46.0",
|
|
26
|
+
"@contrast/dep-hooks": "1.15.0",
|
|
27
27
|
"@contrast/distringuish": "^5.1.0",
|
|
28
|
-
"@contrast/instrumentation": "1.
|
|
29
|
-
"@contrast/logger": "1.
|
|
30
|
-
"@contrast/patcher": "1.
|
|
31
|
-
"@contrast/rewriter": "1.
|
|
32
|
-
"@contrast/route-coverage": "1.
|
|
33
|
-
"@contrast/scopes": "1.
|
|
28
|
+
"@contrast/instrumentation": "1.25.0",
|
|
29
|
+
"@contrast/logger": "1.19.0",
|
|
30
|
+
"@contrast/patcher": "1.18.0",
|
|
31
|
+
"@contrast/rewriter": "1.22.0",
|
|
32
|
+
"@contrast/route-coverage": "1.36.0",
|
|
33
|
+
"@contrast/scopes": "1.16.0",
|
|
34
34
|
"semver": "^7.6.0"
|
|
35
35
|
}
|
|
36
36
|
}
|