@contrast/assess 1.20.2 → 1.22.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.
|
@@ -72,9 +72,11 @@ module.exports = function(core) {
|
|
|
72
72
|
|
|
73
73
|
if (isString(params)) {
|
|
74
74
|
params.split('&').forEach((query) => {
|
|
75
|
-
const startIdx = query.indexOf('?') + 1;
|
|
76
75
|
const endIdx = query.indexOf('=');
|
|
77
|
-
|
|
76
|
+
// this is pretty ugly because we don't want to create a propagation
|
|
77
|
+
// event by splitting off the '?'. so we count on if it's there, we
|
|
78
|
+
// skip it and if it's not there, we start at index 0.
|
|
79
|
+
const key = query.substring(query.indexOf('?') + 1, endIdx);
|
|
78
80
|
const param = query.substring(endIdx + 1, query.length);
|
|
79
81
|
|
|
80
82
|
const keyInfo = tracker.getData(key);
|
|
@@ -19,6 +19,7 @@ const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
|
19
19
|
module.exports = function(core) {
|
|
20
20
|
const express = core.assess.dataflow.sinks.express = {};
|
|
21
21
|
|
|
22
|
+
require('./reflected-xss')(core);
|
|
22
23
|
require('./unvalidated-redirect')(core);
|
|
23
24
|
|
|
24
25
|
express.install = function() {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2024 Contrast Security, Inc
|
|
3
|
+
* Contact: support@contrastsecurity.com
|
|
4
|
+
* License: Commercial
|
|
5
|
+
|
|
6
|
+
* NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
* used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
* made available through public repositories, use of this Software is subject to
|
|
9
|
+
* the applicable End User Licensing Agreement found at
|
|
10
|
+
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
* between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
* way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const util = require('util');
|
|
19
|
+
const {
|
|
20
|
+
Rule: { REFLECTED_XSS: ruleId },
|
|
21
|
+
DataflowTag: {
|
|
22
|
+
UNTRUSTED,
|
|
23
|
+
COOKIE,
|
|
24
|
+
HEADER,
|
|
25
|
+
LIMITED_CHARS,
|
|
26
|
+
ALPHANUM_SPACE_HYPHEN,
|
|
27
|
+
HTML_ENCODED,
|
|
28
|
+
SQL_ENCODED,
|
|
29
|
+
URL_ENCODED,
|
|
30
|
+
WEAK_URL_ENCODED
|
|
31
|
+
},
|
|
32
|
+
isString
|
|
33
|
+
} = require('@contrast/common');
|
|
34
|
+
const { patchType, filterSafeTags } = require('../../common');
|
|
35
|
+
const { InstrumentationType: { RULE } } = require('../../../../constants');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {{
|
|
39
|
+
* assess: import('@contrast/assess').Assess,
|
|
40
|
+
* config: import('@contrast/config').Config,
|
|
41
|
+
* logger: import('@contrast/logger').Logger,
|
|
42
|
+
* }} core
|
|
43
|
+
* @returns {import('@contrast/common').Installable}
|
|
44
|
+
*/
|
|
45
|
+
module.exports = function(core) {
|
|
46
|
+
const {
|
|
47
|
+
depHooks,
|
|
48
|
+
patcher,
|
|
49
|
+
config,
|
|
50
|
+
assess: {
|
|
51
|
+
getSourceContext,
|
|
52
|
+
eventFactory: { createSinkEvent },
|
|
53
|
+
dataflow: {
|
|
54
|
+
tracker,
|
|
55
|
+
sinks: { isVulnerable, reportFindings, reportSafePositive }
|
|
56
|
+
},
|
|
57
|
+
ruleScopes,
|
|
58
|
+
},
|
|
59
|
+
} = core;
|
|
60
|
+
|
|
61
|
+
const reflectedXss = core.assess.dataflow.sinks.express.reflectedXss = {};
|
|
62
|
+
const inspect = patcher.unwrap(util.inspect);
|
|
63
|
+
|
|
64
|
+
const safeTags = [
|
|
65
|
+
COOKIE,
|
|
66
|
+
HEADER,
|
|
67
|
+
LIMITED_CHARS,
|
|
68
|
+
ALPHANUM_SPACE_HYPHEN,
|
|
69
|
+
HTML_ENCODED,
|
|
70
|
+
SQL_ENCODED,
|
|
71
|
+
URL_ENCODED,
|
|
72
|
+
WEAK_URL_ENCODED
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
reflectedXss.install = function() {
|
|
76
|
+
depHooks.resolve({ name: 'express', file: 'lib/response' }, (Response) => {
|
|
77
|
+
['send', 'push'].forEach((method) => {
|
|
78
|
+
const name = `Express.Response.${method}`;
|
|
79
|
+
patcher.patch(Response, method, {
|
|
80
|
+
name,
|
|
81
|
+
patchType,
|
|
82
|
+
around: (next, data) => {
|
|
83
|
+
if (!getSourceContext(RULE, ruleId)) return next();
|
|
84
|
+
|
|
85
|
+
const [str] = data.args;
|
|
86
|
+
|
|
87
|
+
if (!str || !isString(str)) return next();
|
|
88
|
+
|
|
89
|
+
const strInfo = tracker.getData(str);
|
|
90
|
+
if (!strInfo) return next();
|
|
91
|
+
|
|
92
|
+
if (strInfo.tags && isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
|
|
93
|
+
const sinkEvent = createSinkEvent({
|
|
94
|
+
args: [{
|
|
95
|
+
tracked: true,
|
|
96
|
+
value: strInfo.value,
|
|
97
|
+
}],
|
|
98
|
+
context: `response.${method}(${inspect(strInfo.value)})`,
|
|
99
|
+
history: [strInfo],
|
|
100
|
+
name,
|
|
101
|
+
moduleName: 'express',
|
|
102
|
+
methodName: `Response.${method}`,
|
|
103
|
+
object: {
|
|
104
|
+
tracked: false,
|
|
105
|
+
value: 'Express.Response',
|
|
106
|
+
},
|
|
107
|
+
result: {
|
|
108
|
+
tracked: false,
|
|
109
|
+
value: undefined,
|
|
110
|
+
},
|
|
111
|
+
tags: strInfo.tags,
|
|
112
|
+
source: 'P0',
|
|
113
|
+
stacktraceOpts: {
|
|
114
|
+
constructorOpt: data.hooked,
|
|
115
|
+
prependFrames: [data.orig]
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (sinkEvent) {
|
|
120
|
+
reportFindings({
|
|
121
|
+
ruleId,
|
|
122
|
+
sinkEvent
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
} else if (config.assess.safe_positives.enable) {
|
|
126
|
+
reportSafePositive({
|
|
127
|
+
name,
|
|
128
|
+
ruleId,
|
|
129
|
+
safeTags: filterSafeTags(safeTags, strInfo),
|
|
130
|
+
strInfo: {
|
|
131
|
+
tags: strInfo.tags,
|
|
132
|
+
value: strInfo.value,
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return ruleScopes.run(ruleId, next);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return reflectedXss;
|
|
144
|
+
};
|
|
@@ -12,25 +12,90 @@
|
|
|
12
12
|
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
13
|
* way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
15
|
'use strict';
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
//
|
|
18
|
+
// This module implements tag range manipulation functions. There are generally
|
|
19
|
+
// two types of functions:
|
|
20
|
+
//
|
|
21
|
+
// wrappers (create*Tags) - these functions take a set of tags and return a new
|
|
22
|
+
// new set of tags. The new set of tags is *always* a new object with new tag
|
|
23
|
+
// ranges, i.e., the input argument is considered immutable. Tag ranges are
|
|
24
|
+
// *always* returned in "resolved" form, meaning adjacent or overlapping ranges
|
|
25
|
+
// have been merged.
|
|
26
|
+
//
|
|
27
|
+
// core functions - these functions are used by the wrappers to implement their
|
|
28
|
+
// function on a single tag's tag ranges.
|
|
29
|
+
//
|
|
30
|
+
// Tag ranges are represented as an array of integers that must be an even
|
|
31
|
+
// length. The even indices are the start of a tag range and the odd indices
|
|
32
|
+
// are the end of a range. *Unlike most JavaScript string functions*, the end
|
|
33
|
+
// index is inclusive. [0, 0] is a tag range of length 1.
|
|
34
|
+
//
|
|
35
|
+
// All tag ranges are expected to be in "resolved" form, meaning adjacent or
|
|
36
|
+
// overlapping ranges have been merged:
|
|
37
|
+
// - [0, 1] not [0, 0, 1, 1]
|
|
38
|
+
// - [0, 10] not [0, 7, 5, 10]
|
|
39
|
+
//
|
|
21
40
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Appends one set of tags to another set of tags.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} firstTags base set of tags
|
|
45
|
+
* @param {object} secondTags tags to be added
|
|
46
|
+
* @param {number} offset offset to apply to the second set of tags. It is
|
|
47
|
+
* the length of the string that firstTags is associated with.
|
|
48
|
+
*
|
|
49
|
+
* @returns {object} - the resulting tag ranges or null if there were no tags.
|
|
50
|
+
*
|
|
51
|
+
* @examples (args => return)
|
|
52
|
+
* - {},null,9 => null
|
|
53
|
+
* - {},{"UNTRUSTED":[0,2]},3 => {"UNTRUSTED":[3,5]}
|
|
54
|
+
* - {"untrusted":[12,18]},{"untrusted":[0,13]},38 => {"untrusted":[12,18,38,51]}
|
|
55
|
+
* - {"untrusted":[0,2]},{},3 => {"untrusted":[0,2]}
|
|
56
|
+
*/
|
|
57
|
+
function createAppendTags(firstTags, secondTags, offset) {
|
|
58
|
+
if (!firstTags && !secondTags) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
25
61
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
62
|
+
const ret = Object.create(null);
|
|
63
|
+
|
|
64
|
+
let tagNames;
|
|
65
|
+
if (!firstTags) {
|
|
66
|
+
tagNames = new Set([...Object.keys(secondTags)]);
|
|
67
|
+
firstTags = {};
|
|
68
|
+
} else if (!secondTags) {
|
|
69
|
+
tagNames = new Set([...Object.keys(firstTags)]);
|
|
70
|
+
secondTags = {};
|
|
71
|
+
} else {
|
|
72
|
+
tagNames = new Set([...Object.keys(firstTags), ...Object.keys(secondTags)]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let any = null;
|
|
76
|
+
for (const tagName of tagNames) {
|
|
77
|
+
const newTagRanges = appendCore(firstTags[tagName], secondTags[tagName], offset);
|
|
78
|
+
|
|
79
|
+
if (newTagRanges) {
|
|
80
|
+
ret[tagName] = newTagRanges;
|
|
81
|
+
any = ret;
|
|
82
|
+
}
|
|
30
83
|
}
|
|
31
84
|
|
|
85
|
+
return any;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function appendCore(firstTagRanges, secondTagRanges, offset) {
|
|
89
|
+
if (!firstTagRanges?.length && !secondTagRanges?.length) {
|
|
90
|
+
return null;
|
|
91
|
+
} else if (!firstTagRanges?.length) {
|
|
92
|
+
return secondTagRanges.map((v) => v + offset);
|
|
93
|
+
} else if (!secondTagRanges?.length) {
|
|
94
|
+
return [...firstTagRanges];
|
|
95
|
+
}
|
|
32
96
|
|
|
33
97
|
const newTagRanges = [...firstTagRanges];
|
|
98
|
+
secondTagRanges = [...secondTagRanges];
|
|
34
99
|
|
|
35
100
|
for (let i = 0; i < secondTagRanges.length; i++) {
|
|
36
101
|
secondTagRanges[i] += offset;
|
|
@@ -38,7 +103,7 @@ function atomicAppend(firstTagRanges, secondTagRanges, offset) {
|
|
|
38
103
|
|
|
39
104
|
const firstTagRangesLastEnd = firstTagRanges[firstTagRanges.length - 1];
|
|
40
105
|
|
|
41
|
-
if (firstTagRangesLastEnd
|
|
106
|
+
if (firstTagRangesLastEnd >= secondTagRanges[0] - 1) {
|
|
42
107
|
newTagRanges.pop();
|
|
43
108
|
secondTagRanges.shift();
|
|
44
109
|
}
|
|
@@ -48,10 +113,85 @@ function atomicAppend(firstTagRanges, secondTagRanges, offset) {
|
|
|
48
113
|
return newTagRanges;
|
|
49
114
|
}
|
|
50
115
|
|
|
51
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Return the tag ranges that overlap the given range. The overlap does not
|
|
118
|
+
* need to be complete, i.e., an intersection causes the entire tag range
|
|
119
|
+
* to match.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} tags tags
|
|
122
|
+
* @param {number} startIndex
|
|
123
|
+
* @param {number} endIndex
|
|
124
|
+
*
|
|
125
|
+
* @returns {object} the overlapping tag ranges. It can be an empty object
|
|
126
|
+
* when there no tags overlap. NOTE: it does not return null when there are
|
|
127
|
+
* no overlapping tags AND it returns tag ranges as arrays of 2-element
|
|
128
|
+
* arrays. Why is lost to history.
|
|
129
|
+
*
|
|
130
|
+
* @examples
|
|
131
|
+
* - {x: [10, 20]}, 15, 17 => { x: [ [ 10, 20 ] ] }
|
|
132
|
+
* - {x: [17, 20]}, 90, 100 => {}
|
|
133
|
+
* - {x: [17, 20]}, 15, 17 => { x: [ [ 17, 20 ] ] }
|
|
134
|
+
*
|
|
135
|
+
*/
|
|
136
|
+
function createOverlappingTags(tags, startIndex, endIndex) {
|
|
137
|
+
const overlappingTags = {};
|
|
138
|
+
|
|
139
|
+
for (const [tag, tagRanges] of Object.entries(tags)) {
|
|
140
|
+
const overlappingRanges = [];
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < tagRanges.length; i += 2) {
|
|
143
|
+
if (tagRanges[i + 1] >= startIndex && tagRanges[i] <= endIndex) {
|
|
144
|
+
overlappingRanges.push([tagRanges[i], tagRanges[i + 1]]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (overlappingRanges.length > 0) {
|
|
149
|
+
overlappingTags[tag] = overlappingRanges;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return overlappingTags;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* this is not intuitive in that it not only returns the subset of tags that exist
|
|
158
|
+
* within the subsetStart/len range, but it also adjusts the tag ranges to be relative
|
|
159
|
+
* to the subsetStart.
|
|
160
|
+
*
|
|
161
|
+
* @param tags tag ranges to adjust
|
|
162
|
+
* @param subsetStart start index of the subset range
|
|
163
|
+
* @param len length of the subset range (subsetStop = subsetStart + len - 1)
|
|
164
|
+
*
|
|
165
|
+
* @returns {object} tags with adjusted ranges or null if no tags overlapped
|
|
166
|
+
* the subset range.
|
|
167
|
+
*
|
|
168
|
+
* @examples (args => return)
|
|
169
|
+
* - {"UNTRUSTED":[0,16]}, 13, 4 => {"UNTRUSTED":[0,3]}
|
|
170
|
+
* - {"x":[0,1],"ampersand":[6,6], "y":[7,8]}, 0, 1 => {"x":[0,0]}
|
|
171
|
+
*/
|
|
172
|
+
function createSubsetTags(tags, subsetStart, len) {
|
|
173
|
+
if (!tags) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const ret = Object.create(null);
|
|
177
|
+
|
|
178
|
+
let some = null;
|
|
179
|
+
for (const tagName of Object.keys(tags)) {
|
|
180
|
+
const newTagRanges = subsetCore(tags[tagName], subsetStart, len);
|
|
181
|
+
if (newTagRanges) {
|
|
182
|
+
ret[tagName] = newTagRanges;
|
|
183
|
+
some = ret;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return some;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function subsetCore(tags, subsetStart, len) {
|
|
52
191
|
const ret = [];
|
|
53
192
|
const subsetStop = subsetStart + len - 1;
|
|
54
193
|
|
|
194
|
+
let some = null;
|
|
55
195
|
for (let idx = 0; idx < tags.length - 1; idx += 2) {
|
|
56
196
|
const tagStart = tags[idx];
|
|
57
197
|
const tagStop = tags[idx + 1];
|
|
@@ -69,12 +209,97 @@ function atomicSubset(tags, subsetStart, len) {
|
|
|
69
209
|
Math.max(tagStart, subsetStart) - subsetStart,
|
|
70
210
|
Math.min(tagStop, subsetStop) - subsetStart,
|
|
71
211
|
);
|
|
212
|
+
some = ret;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return some;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Create new tags from 0 to the specified length for each tag name in the
|
|
220
|
+
* input tags.
|
|
221
|
+
*
|
|
222
|
+
* @param {object} tags tags names to use
|
|
223
|
+
* @param {number} resultLength length for new tag ranges.
|
|
224
|
+
*
|
|
225
|
+
* @returns {object} new tags or null if no tags
|
|
226
|
+
*
|
|
227
|
+
* @examples (args => return)
|
|
228
|
+
* - {"UNTRUSTED":[0,18]}, 24 => {"UNTRUSTED":[0,23]}
|
|
229
|
+
* - {"UNTRUSTED":[0,4], "URL_ENCODED":[0,0]}, 33 => {"UNTRUSTED":[0,32],"URL_ENCODED":[0,32]}
|
|
230
|
+
*/
|
|
231
|
+
function createFullLengthCopyTags(tags, resultLength) {
|
|
232
|
+
if (!resultLength || resultLength < 0 || !tags) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const tagNames = Object.keys(tags);
|
|
237
|
+
if (!tagNames.length) {
|
|
238
|
+
// no need to check result after the loop
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
resultLength = resultLength - 1;
|
|
243
|
+
|
|
244
|
+
const ret = Object.create(null);
|
|
245
|
+
for (const tagName of tagNames) {
|
|
246
|
+
ret[tagName] = [0, resultLength];
|
|
72
247
|
}
|
|
73
248
|
|
|
74
249
|
return ret;
|
|
75
250
|
}
|
|
76
251
|
|
|
77
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Create the union of two sets of tags.
|
|
254
|
+
*
|
|
255
|
+
* @param {object} firstTags first set of tags
|
|
256
|
+
* @param {object} secondTags second set of tags
|
|
257
|
+
*
|
|
258
|
+
* @returns {object} new tags or null if no tags
|
|
259
|
+
*
|
|
260
|
+
* @examples (args => return)
|
|
261
|
+
* - {},null => null
|
|
262
|
+
* - {}, {} => null
|
|
263
|
+
* - {},{"UNTRUSTED":[5,7]} => {"UNTRUSTED":[5,7]}
|
|
264
|
+
* - {"UNTRUSTED":[14,22]},{"UNTRUSTED":[11,13]} => {"UNTRUSTED":[11,22]}
|
|
265
|
+
* - {"UNTRUSTED":[5,20]},{"UNTRUSTED":[0,4]} => {"UNTRUSTED":[0,20]}
|
|
266
|
+
*/
|
|
267
|
+
function createMergedTags(firstTags, secondTags) {
|
|
268
|
+
const ret = Object.create(null);
|
|
269
|
+
|
|
270
|
+
if (!firstTags && !secondTags) {
|
|
271
|
+
return null;
|
|
272
|
+
} else if (!firstTags) {
|
|
273
|
+
return copyTags(secondTags);
|
|
274
|
+
} else if (!secondTags) {
|
|
275
|
+
return copyTags(firstTags);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// only do set building and merging if both sets of tags are not empty
|
|
279
|
+
const tagNames = new Set([...Object.keys(firstTags), ...Object.keys(secondTags)]);
|
|
280
|
+
|
|
281
|
+
let some = null;
|
|
282
|
+
for (const tagName of tagNames) {
|
|
283
|
+
const newTagRanges = mergeCore(firstTags[tagName], secondTags[tagName]);
|
|
284
|
+
|
|
285
|
+
if (newTagRanges.length) {
|
|
286
|
+
some = ret;
|
|
287
|
+
ret[tagName] = newTagRanges;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return some;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function mergeCore(firstTagRanges, secondTagRanges) {
|
|
295
|
+
if (!firstTagRanges && !secondTagRanges) {
|
|
296
|
+
return [];
|
|
297
|
+
} else if (!firstTagRanges) {
|
|
298
|
+
return [...secondTagRanges];
|
|
299
|
+
} else if (!secondTagRanges) {
|
|
300
|
+
return [...firstTagRanges];
|
|
301
|
+
}
|
|
302
|
+
|
|
78
303
|
const mergedRanges = [];
|
|
79
304
|
let i = 0;
|
|
80
305
|
let j = 0;
|
|
@@ -125,7 +350,52 @@ function atomicMerge(firstTagRanges, secondTagRanges) {
|
|
|
125
350
|
return finalMergedRanges;
|
|
126
351
|
}
|
|
127
352
|
|
|
128
|
-
function
|
|
353
|
+
function copyTags(tags) {
|
|
354
|
+
const ret = Object.create(null);
|
|
355
|
+
|
|
356
|
+
let some = null;
|
|
357
|
+
for (const tagName of Object.keys(tags)) {
|
|
358
|
+
ret[tagName] = [...tags[tagName]];
|
|
359
|
+
some = ret;
|
|
360
|
+
}
|
|
361
|
+
return some;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Create new tags that exclude the given range.
|
|
366
|
+
*
|
|
367
|
+
* @param {object} tags tags used to create new tags
|
|
368
|
+
* @param {number} exclusionRange the new tags will not include this range
|
|
369
|
+
*
|
|
370
|
+
* @returns {object} new tags or null if no tags after excluding the range
|
|
371
|
+
*
|
|
372
|
+
* @examples (args => return)
|
|
373
|
+
* - {},[13,13] => null
|
|
374
|
+
* - {"UNTRUSTED":[0,8]},[6,6] => {"UNTRUSTED":[0,5,7,8]}
|
|
375
|
+
* - {"UNTRUSTED":[13,16]},[13,13] => {"UNTRUSTED":[14,16]}
|
|
376
|
+
*/
|
|
377
|
+
function createTagsWithExclusion(tags, exclusionRange) {
|
|
378
|
+
// if no exclusionRange or no tags, there's nothing to do
|
|
379
|
+
if (!exclusionRange.length || !tags) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const ret = Object.create(null);
|
|
384
|
+
let some = null;
|
|
385
|
+
|
|
386
|
+
for (const tagName of Object.keys(tags)) {
|
|
387
|
+
const newTagRanges = excludeRangeCore(tags[tagName], exclusionRange);
|
|
388
|
+
|
|
389
|
+
if (newTagRanges.length) {
|
|
390
|
+
ret[tagName] = newTagRanges;
|
|
391
|
+
some = ret;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return some;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function excludeRangeCore(tags, exclusionRange) {
|
|
129
399
|
const ret = [];
|
|
130
400
|
const [exclusionStart, exclusionStop] = exclusionRange;
|
|
131
401
|
|
|
@@ -134,17 +404,18 @@ function atomicExclude(tags, exclusionRange) {
|
|
|
134
404
|
const tagStop = tags[idx + 1];
|
|
135
405
|
|
|
136
406
|
if (tagStop < exclusionStart) {
|
|
407
|
+
// exclusion is below - continue to check next range
|
|
137
408
|
ret.push(tagStart, tagStop);
|
|
138
|
-
// exlusion is below - continue to check next range
|
|
139
409
|
continue;
|
|
140
410
|
}
|
|
141
411
|
|
|
142
412
|
if (tagStart > exclusionStop) {
|
|
143
|
-
ret.push(...tags.slice(idx));
|
|
144
413
|
// all other ranges are above exclusion so we can stop
|
|
414
|
+
ret.push(...tags.slice(idx));
|
|
145
415
|
break;
|
|
146
416
|
}
|
|
147
417
|
|
|
418
|
+
// this tag range overlaps the exclusion range
|
|
148
419
|
if (exclusionStart <= tagStart && exclusionStop < tagStop) {
|
|
149
420
|
ret.push(exclusionStop + 1, tagStop);
|
|
150
421
|
}
|
|
@@ -161,122 +432,57 @@ function atomicExclude(tags, exclusionRange) {
|
|
|
161
432
|
return ret;
|
|
162
433
|
}
|
|
163
434
|
|
|
164
|
-
function createAppendTags(firstTags, secondTags, offset) {
|
|
165
|
-
const ret = Object.create(null);
|
|
166
|
-
const firstTagsObject = ensureObject(firstTags);
|
|
167
|
-
const secondTagsObject = ensureObject(secondTags);
|
|
168
|
-
const tagNames = new Set([...Object.keys(firstTagsObject), ...Object.keys(secondTagsObject)]);
|
|
169
|
-
|
|
170
|
-
for (const tagName of tagNames) {
|
|
171
|
-
const newTagRanges = atomicAppend(ensureTagsImmutable(firstTagsObject, tagName), ensureTagsImmutable(secondTagsObject, tagName), offset);
|
|
172
|
-
|
|
173
|
-
newTagRanges.length && (ret[tagName] = newTagRanges);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return Object.keys(ret).length ? ret : null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function createOverlappingTags(tags, startIndex, endIndex) {
|
|
180
|
-
const overlappingTags = {};
|
|
181
|
-
|
|
182
|
-
Object.entries(tags).forEach(([tag, tagRanges]) => {
|
|
183
|
-
const overlappingRanges = [];
|
|
184
|
-
|
|
185
|
-
for (let i = 0; i < tagRanges.length; i += 2) {
|
|
186
|
-
const start = tagRanges[i];
|
|
187
|
-
const end = tagRanges[i + 1];
|
|
188
|
-
|
|
189
|
-
if (end >= startIndex && start <= endIndex) {
|
|
190
|
-
overlappingRanges.push([start, end]);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (overlappingRanges.length > 0) {
|
|
195
|
-
overlappingTags[tag] = overlappingRanges;
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
return overlappingTags;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
435
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return Object.keys(ret).length ? ret : null;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function createFullLengthCopyTags(tags, resultLength) {
|
|
224
|
-
if (!resultLength || resultLength <= 0) return null;
|
|
225
|
-
const ret = Object.create(null);
|
|
226
|
-
|
|
227
|
-
for (const tagName of Object.keys(ensureObject(tags))) {
|
|
228
|
-
ret[tagName] = [0, resultLength - 1];
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return Object.keys(ret).length ? ret : null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function createMergedTags(firstTags, secondTags) {
|
|
235
|
-
const ret = Object.create(null);
|
|
236
|
-
const firstTagsObject = ensureObject(firstTags);
|
|
237
|
-
const secondTagsObject = ensureObject(secondTags);
|
|
238
|
-
const tagNames = new Set([...Object.keys(firstTagsObject), ...Object.keys(secondTagsObject)]);
|
|
239
|
-
|
|
240
|
-
for (const tagName of tagNames) {
|
|
241
|
-
const newTagRanges = atomicMerge(ensureTagsImmutable(firstTagsObject, tagName), ensureTagsImmutable(secondTagsObject, tagName));
|
|
242
|
-
|
|
243
|
-
newTagRanges.length && (ret[tagName] = newTagRanges);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return Object.keys(ret).length ? ret : null;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function createTagsWithExclusion(tags, exclusionRange) {
|
|
250
|
-
if (!exclusionRange.length) return;
|
|
251
|
-
|
|
252
|
-
const ret = Object.create(null);
|
|
253
|
-
const tagsObject = ensureObject(tags);
|
|
254
|
-
|
|
255
|
-
for (const tagName of Object.keys(tagsObject)) {
|
|
256
|
-
const newTagRanges = atomicExclude(ensureTagsImmutable(tagsObject, tagName), exclusionRange);
|
|
257
|
-
|
|
258
|
-
newTagRanges.length && (ret[tagName] = newTagRanges);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return Object.keys(ret).length ? ret : null;
|
|
262
|
-
}
|
|
263
|
-
|
|
436
|
+
* Create tags for strings inserted into mongo queries.
|
|
437
|
+
* It also appears in vm but that is not evaluated during testing.
|
|
438
|
+
*
|
|
439
|
+
* @param {array | ?} path sequence of keys
|
|
440
|
+
* @param {object} tags tags to adjust. They refer to the user input.
|
|
441
|
+
* @param {string} value the user input
|
|
442
|
+
* @param {string} argString string incorporating the user input. The tag ranges
|
|
443
|
+
* in the returned tags refer to this string.
|
|
444
|
+
*
|
|
445
|
+
* @returns {object} new tags or null if no tags
|
|
446
|
+
*
|
|
447
|
+
* @examples
|
|
448
|
+
* - ["query"],{"UNTRUSTED":[0,2]},"foo","{ query: 'foo' }" => {"UNTRUSTED":[10,12]}
|
|
449
|
+
* - ["val"],{"UNTRUSTED":[0,3],"CUSTOM_VALIDATED":[0,3]},"SAFE","{ val: 'SAFE' }" => {"UNTRUSTED":[8,11],"CUSTOM_VALIDATED":[8,11]}
|
|
450
|
+
*
|
|
451
|
+
*/
|
|
264
452
|
function createAdjustedQueryTags(path, tags, value, argString) {
|
|
265
453
|
let idx = -1;
|
|
266
454
|
for (const str of [...path, value]) {
|
|
267
|
-
// This is the case where the
|
|
455
|
+
// This is the case where the input is an array
|
|
268
456
|
if (str === 0) continue;
|
|
269
457
|
|
|
270
458
|
idx = argString.indexOf(str, idx);
|
|
271
|
-
if (idx
|
|
459
|
+
if (idx < 0) {
|
|
272
460
|
idx = -1;
|
|
273
461
|
break;
|
|
274
462
|
}
|
|
275
463
|
}
|
|
276
464
|
|
|
277
|
-
return idx
|
|
465
|
+
return idx >= 0 ? createAppendTags([], tags, idx) : [...tags];
|
|
278
466
|
}
|
|
279
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Adjust tag ranges for escaped characters in original text. This typically
|
|
470
|
+
* involves splitting a tag range into multiple ranges. I'm not sure this is
|
|
471
|
+
* particularly useful, because the "vulnerable" text has been escaped so, even
|
|
472
|
+
* if the original text is present it's unlikely to be vulnerable.
|
|
473
|
+
*
|
|
474
|
+
* @param {string} input original text
|
|
475
|
+
* @param {string} result escaped text
|
|
476
|
+
* @param {object} tags tags to adjust by removing escaped characters from
|
|
477
|
+
* the tag ranges.
|
|
478
|
+
*
|
|
479
|
+
* @returns {object} new tags
|
|
480
|
+
*
|
|
481
|
+
* @examples (args => return)
|
|
482
|
+
* - "foo","bar",{"untrusted":[0,2]} => [] // wtf?
|
|
483
|
+
* - "<foo>","<foo>",{"untrusted":[1,3]} => {"untrusted":[4,6]}
|
|
484
|
+
* - "?test=str","%3Ftest%3Dstr",{"UNTRUSTED":[0,8]} => {"UNTRUSTED":[3,6,10,12]}
|
|
485
|
+
*/
|
|
280
486
|
function createEscapeTagRanges(input, result, tags) {
|
|
281
487
|
const inputArr = input.split('');
|
|
282
488
|
const escapedArr = result.split('');
|
|
@@ -285,8 +491,10 @@ function createEscapeTagRanges(input, result, tags) {
|
|
|
285
491
|
return x;
|
|
286
492
|
}
|
|
287
493
|
});
|
|
494
|
+
const ret = Object.create(null);
|
|
495
|
+
|
|
288
496
|
if (overlap.length === 0) {
|
|
289
|
-
return
|
|
497
|
+
return ret;
|
|
290
498
|
}
|
|
291
499
|
const newTagRanges = [];
|
|
292
500
|
let firstIndex = escapedArr.indexOf(overlap[0]);
|
|
@@ -304,13 +512,14 @@ function createEscapeTagRanges(input, result, tags) {
|
|
|
304
512
|
currIndex = nextIndex;
|
|
305
513
|
}
|
|
306
514
|
|
|
307
|
-
const ret = Object.create(null);
|
|
308
515
|
for (const tagName of Object.keys(tags)) {
|
|
309
516
|
ret[tagName] = newTagRanges;
|
|
310
517
|
}
|
|
518
|
+
|
|
311
519
|
return ret;
|
|
312
520
|
}
|
|
313
521
|
|
|
522
|
+
|
|
314
523
|
module.exports = {
|
|
315
524
|
createSubsetTags,
|
|
316
525
|
createAppendTags,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/assess",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.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)",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"test": "../scripts/test.sh"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@contrast/common": "1.
|
|
20
|
+
"@contrast/common": "1.17.0",
|
|
21
21
|
"@contrast/distringuish": "^4.4.0",
|
|
22
22
|
"@contrast/scopes": "1.4.0"
|
|
23
23
|
}
|