@contrast/assess 1.78.0 → 1.79.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/index.js +2 -0
- package/lib/dataflow/propagation/install/JSON/stringify.js +14 -141
- package/lib/dataflow/propagation/install/JSON/tag-ranges.js +174 -0
- package/lib/dataflow/propagation/install/jsonwebtoken.js +1 -1
- package/lib/dataflow/propagation/install/sanitize-html.js +1 -1
- package/lib/dataflow/propagation/install/url/url.js +56 -22
- package/lib/dataflow/propagation/install/util-format.js +43 -3
- package/lib/dataflow/propagation/install/zod/matrix.js +149 -0
- package/lib/dataflow/propagation/install/zod/process-result-v3.js +228 -0
- package/lib/dataflow/propagation/install/zod/process-result-v4.js +191 -0
- package/lib/dataflow/propagation/install/zod/utils.js +227 -0
- package/lib/dataflow/propagation/install/zod/v3.js +117 -0
- package/lib/dataflow/propagation/install/zod/v4.js +97 -0
- package/lib/dataflow/sources/handler.js +1 -1
- package/lib/dataflow/sources/install/@sap.js +14 -2
- package/lib/dataflow/sources/install/fastify-websocket.js +1 -1
- package/lib/dataflow/sources/install/socket.io.js +1 -1
- package/lib/event-factory.js +1 -1
- package/lib/get-source-context.js +1 -1
- package/lib/index.js +1 -1
- package/lib/make-source-context.js +1 -1
- package/lib/policy.js +1 -1
- package/package.json +12 -12
|
@@ -49,6 +49,8 @@ module.exports = function(core) {
|
|
|
49
49
|
require('./install/path')(core);
|
|
50
50
|
require('./install/reg-exp-prototype-exec')(core);
|
|
51
51
|
require('./install/joi')(core);
|
|
52
|
+
require('./install/zod/v4')(core);
|
|
53
|
+
require('./install/zod/v3')(core);
|
|
52
54
|
require('./install/util-format')(core);
|
|
53
55
|
|
|
54
56
|
propagation.install = function() {
|
|
@@ -16,67 +16,20 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const {
|
|
19
|
-
isString,
|
|
20
19
|
primordials: {
|
|
21
20
|
ArrayPrototypeSlice,
|
|
22
|
-
StringPrototypeReplace,
|
|
23
|
-
StringPrototypeMatch,
|
|
24
|
-
StringPrototypeMatchAll,
|
|
25
|
-
StringPrototypeSlice,
|
|
26
21
|
}
|
|
27
22
|
} = require('@contrast/common');
|
|
28
|
-
const crypto = require('crypto');
|
|
29
23
|
const { createMergedTags, getAdjustedUntrackedValue, truncateStringValue } = require('../../../tag-utils');
|
|
30
24
|
const { patchType } = require('../../common');
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const canary = makeCanary();
|
|
43
|
-
const regex = new RegExp(`"${canary}(\\d+)~`, 'g');
|
|
44
|
-
function identity(_key, val) {
|
|
45
|
-
return val;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function patchReplacer(replacer) {
|
|
49
|
-
if (Array.isArray(replacer)) {
|
|
50
|
-
// keys that are numbers get cast to string when they come through the replacer
|
|
51
|
-
// add '' to the list of keys as that's always the first iteration of replacer, the entire json string
|
|
52
|
-
const filterKeys = replacer
|
|
53
|
-
.filter((key) => isString(key) || typeof key === 'number')
|
|
54
|
-
.concat('')
|
|
55
|
-
.map((value) => value.toString());
|
|
56
|
-
|
|
57
|
-
let nextArray = [];
|
|
58
|
-
replacer = function(key, value) {
|
|
59
|
-
// we need to keep track of arrays
|
|
60
|
-
// and shift each element as the replacer hits it
|
|
61
|
-
// so we can compare those values are expected
|
|
62
|
-
// when filtering
|
|
63
|
-
const nextEl = nextArray.shift();
|
|
64
|
-
if (Array.isArray(value)) {
|
|
65
|
-
nextArray = [...value];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (filterKeys.includes(key) || value === nextEl) {
|
|
69
|
-
return value;
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
} else if (typeof replacer === 'function') {
|
|
73
|
-
// do nothing!
|
|
74
|
-
} else {
|
|
75
|
-
replacer = identity;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return replacer;
|
|
79
|
-
}
|
|
25
|
+
const {
|
|
26
|
+
regex,
|
|
27
|
+
patchReplacer,
|
|
28
|
+
getUntrustedSpaceProps,
|
|
29
|
+
createSpaceTagRanges,
|
|
30
|
+
markCanaryValue,
|
|
31
|
+
applyCanaryTagRanges,
|
|
32
|
+
} = require('./tag-ranges');
|
|
80
33
|
|
|
81
34
|
module.exports = function(core) {
|
|
82
35
|
const {
|
|
@@ -88,77 +41,6 @@ module.exports = function(core) {
|
|
|
88
41
|
}
|
|
89
42
|
} = core;
|
|
90
43
|
|
|
91
|
-
function getUntrustedSpaceProps(space) {
|
|
92
|
-
// it can't be a problem if it's not a string, if the string is all spaces, or
|
|
93
|
-
// if the string is zero length.
|
|
94
|
-
if (!isString(space) || StringPrototypeMatch.call(space, /^\s+$/) || !space) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
const props = tracker.getData(StringPrototypeSlice.call(space, 0, 10));
|
|
98
|
-
if (!props || !Object.keys(props.tags).length) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return props;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
function createSpaceTagRanges(result, metadata) {
|
|
107
|
-
const tags = {};
|
|
108
|
-
const spaceValue = StringPrototypeSlice.call(metadata.origArgs[2], 0, 10);
|
|
109
|
-
const { spaceProps: { tags: spacePropsTags } } = metadata;
|
|
110
|
-
|
|
111
|
-
const spaces = Array.from(StringPrototypeMatchAll.call(result, new RegExp(`(?<=\\n)(${spaceValue})+?(?=\\d|"|null|]|})`, 'g')));
|
|
112
|
-
|
|
113
|
-
for (const space of spaces) {
|
|
114
|
-
const match = space[0];
|
|
115
|
-
const { index } = space;
|
|
116
|
-
for (const tagKey of Object.keys(spacePropsTags)) {
|
|
117
|
-
if (!tags[tagKey]) {
|
|
118
|
-
tags[tagKey] = [];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (space !== spaceValue && (spacePropsTags[tagKey].length !== 2 || spacePropsTags[tagKey][1] !== spaceValue.length - 1)) {
|
|
122
|
-
for (let i = 0; i < match.length / spaceValue.length; i++) {
|
|
123
|
-
tags[tagKey].push(...spacePropsTags[tagKey].map((rangeIdx) => index + (rangeIdx + i * spaceValue.length)));
|
|
124
|
-
}
|
|
125
|
-
} else {
|
|
126
|
-
tags[tagKey].push(index, index + match.length - 1);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return tags;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function applyCanaryTagRanges(result, metadata, tags) {
|
|
135
|
-
metadata.sourceEvents = [];
|
|
136
|
-
// the diff doesn't include the " that opens the regex.
|
|
137
|
-
let canaryReplacementDiff = 0;
|
|
138
|
-
// for each marker in the stringify's result, remove the marker and
|
|
139
|
-
// create a TagRange for the value.
|
|
140
|
-
return StringPrototypeReplace.call(result, metadata.regex, (m, id, offset) => {
|
|
141
|
-
// adjust offset by total characters removed so far
|
|
142
|
-
offset = offset - canaryReplacementDiff + 1;
|
|
143
|
-
// we don't replace the opening " in the regex - that just makes sure
|
|
144
|
-
// we only find our marker at the start of a string value.
|
|
145
|
-
canaryReplacementDiff += m.length - 1;
|
|
146
|
-
for (const tagKey of Object.keys(metadata.strInfos[id].tags)) {
|
|
147
|
-
if (!tags[tagKey]) {
|
|
148
|
-
tags[tagKey] = [];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
for (const tagIdx of metadata.strInfos[id].tags[tagKey]) {
|
|
152
|
-
tags[tagKey].push(tagIdx + offset);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
metadata.history.add(metadata.strInfos[id]);
|
|
157
|
-
// replace the canary with the starting quote
|
|
158
|
-
return '"';
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
44
|
return core.assess.dataflow.propagation.jsonInstrumentation.stringify = {
|
|
163
45
|
install() {
|
|
164
46
|
patcher.patch(JSON, 'stringify', {
|
|
@@ -186,16 +68,13 @@ module.exports = function(core) {
|
|
|
186
68
|
|
|
187
69
|
// if the space can't be trusted then remember space's tracking info so it
|
|
188
70
|
// can be applied to the entire JSON.stringify result string.
|
|
189
|
-
const spaceProps = getUntrustedSpaceProps(space);
|
|
71
|
+
const spaceProps = getUntrustedSpaceProps(tracker, space);
|
|
190
72
|
if (spaceProps) {
|
|
191
73
|
data.metadata.spaceProps = spaceProps;
|
|
192
74
|
data.metadata.history.add(spaceProps);
|
|
193
75
|
data.metadata.propagate = true;
|
|
194
76
|
}
|
|
195
77
|
|
|
196
|
-
// each canary tag gets a separate id to make matching easier.
|
|
197
|
-
let id = 0;
|
|
198
|
-
|
|
199
78
|
/**
|
|
200
79
|
* wraps the replacer method and updates propagate boolean if value of replacer
|
|
201
80
|
* is tracked
|
|
@@ -204,20 +83,14 @@ module.exports = function(core) {
|
|
|
204
83
|
* @param {string} value
|
|
205
84
|
*/
|
|
206
85
|
function contrastReplacer(key, val) {
|
|
207
|
-
const
|
|
208
|
-
|
|
86
|
+
const value = replacer.call(this, key, val);
|
|
87
|
+
const marked = markCanaryValue(tracker, data.metadata.strInfos, val, value);
|
|
88
|
+
|
|
89
|
+
if (marked.propagate) {
|
|
209
90
|
data.metadata.propagate = true;
|
|
210
91
|
}
|
|
211
92
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// keys aren't propagated, so check only the value
|
|
215
|
-
if (isString(value) && valProperties) {
|
|
216
|
-
data.metadata.strInfos[id] = valProperties;
|
|
217
|
-
value = `${canary}${id}~${value}`;
|
|
218
|
-
id++;
|
|
219
|
-
}
|
|
220
|
-
return value;
|
|
93
|
+
return marked.value;
|
|
221
94
|
}
|
|
222
95
|
|
|
223
96
|
data.args = [input, contrastReplacer, space];
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2026 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 {
|
|
19
|
+
isString,
|
|
20
|
+
primordials: {
|
|
21
|
+
StringPrototypeReplace,
|
|
22
|
+
StringPrototypeMatch,
|
|
23
|
+
StringPrototypeMatchAll,
|
|
24
|
+
StringPrototypeSlice,
|
|
25
|
+
}
|
|
26
|
+
} = require('@contrast/common');
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
|
|
29
|
+
// Shared functions between JSON.stringify instrumentation
|
|
30
|
+
// and util.format when 'j' format string is used
|
|
31
|
+
// for keeping accurate tag ranges
|
|
32
|
+
|
|
33
|
+
function makeCanary() {
|
|
34
|
+
return StringPrototypeReplace.call(
|
|
35
|
+
crypto
|
|
36
|
+
.randomBytes(12)
|
|
37
|
+
.toString('base64'),
|
|
38
|
+
/[+\\]/g,
|
|
39
|
+
(match) => match === '+' ? '%' : '#'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const canary = makeCanary();
|
|
44
|
+
const regex = new RegExp(`"${canary}(\\d+)~`, 'g');
|
|
45
|
+
|
|
46
|
+
function identity(_key, val) {
|
|
47
|
+
return val;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function patchReplacer(replacer) {
|
|
51
|
+
if (Array.isArray(replacer)) {
|
|
52
|
+
// keys that are numbers get cast to string when they come through the replacer
|
|
53
|
+
// add '' to the list of keys as that's always the first iteration of replacer, the entire json string
|
|
54
|
+
const filterKeys = replacer
|
|
55
|
+
.filter((key) => isString(key) || typeof key === 'number')
|
|
56
|
+
.concat('')
|
|
57
|
+
.map((value) => value.toString());
|
|
58
|
+
|
|
59
|
+
let nextArray = [];
|
|
60
|
+
replacer = function(key, value) {
|
|
61
|
+
// we need to keep track of arrays
|
|
62
|
+
// and shift each element as the replacer hits it
|
|
63
|
+
// so we can compare those values are expected
|
|
64
|
+
// when filtering
|
|
65
|
+
const nextEl = nextArray.shift();
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
nextArray = [...value];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (filterKeys.includes(key) || value === nextEl) {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
} else if (typeof replacer === 'function') {
|
|
75
|
+
// do nothing!
|
|
76
|
+
} else {
|
|
77
|
+
replacer = identity;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return replacer;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getUntrustedSpaceProps(tracker, space) {
|
|
84
|
+
// it can't be a problem if it's not a string, if the string is all spaces, or
|
|
85
|
+
// if the string is zero length.
|
|
86
|
+
if (!isString(space) || StringPrototypeMatch.call(space, /^\s+$/) || !space) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const props = tracker.getData(StringPrototypeSlice.call(space, 0, 10));
|
|
90
|
+
if (!props || !Object.keys(props.tags).length) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return props;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createSpaceTagRanges(result, metadata) {
|
|
98
|
+
const tags = {};
|
|
99
|
+
const spaceValue = StringPrototypeSlice.call(metadata.origArgs[2], 0, 10);
|
|
100
|
+
const { spaceProps: { tags: spacePropsTags } } = metadata;
|
|
101
|
+
|
|
102
|
+
const spaces = Array.from(StringPrototypeMatchAll.call(result, new RegExp(`(?<=\\n)(${spaceValue})+?(?=\\d|"|null|]|})`, 'g')));
|
|
103
|
+
|
|
104
|
+
for (const space of spaces) {
|
|
105
|
+
const match = space[0];
|
|
106
|
+
const { index } = space;
|
|
107
|
+
for (const tagKey of Object.keys(spacePropsTags)) {
|
|
108
|
+
if (!tags[tagKey]) {
|
|
109
|
+
tags[tagKey] = [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (space !== spaceValue && (spacePropsTags[tagKey].length !== 2 || spacePropsTags[tagKey][1] !== spaceValue.length - 1)) {
|
|
113
|
+
for (let i = 0; i < match.length / spaceValue.length; i++) {
|
|
114
|
+
tags[tagKey].push(...spacePropsTags[tagKey].map((rangeIdx) => index + (rangeIdx + i * spaceValue.length)));
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
tags[tagKey].push(index, index + match.length - 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return tags;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function markCanaryValue(tracker, strInfos, val, value) {
|
|
126
|
+
const valInfo = tracker.getData(val);
|
|
127
|
+
const propagate = !!(valInfo && Object.keys(valInfo.tags).length);
|
|
128
|
+
|
|
129
|
+
if (isString(value) && valInfo) {
|
|
130
|
+
const id = Object.keys(strInfos).length;
|
|
131
|
+
strInfos[id] = valInfo;
|
|
132
|
+
value = `${canary}${id}~${value}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { value, propagate };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function applyCanaryTagRanges(result, metadata, tags) {
|
|
139
|
+
metadata.sourceEvents = [];
|
|
140
|
+
// the diff doesn't include the " that opens the regex.
|
|
141
|
+
let canaryReplacementDiff = 0;
|
|
142
|
+
// for each marker in the stringify's result, remove the marker and
|
|
143
|
+
// create a TagRange for the value.
|
|
144
|
+
return StringPrototypeReplace.call(result, metadata.regex, (m, id, offset) => {
|
|
145
|
+
// adjust offset by total characters removed so far
|
|
146
|
+
offset = offset - canaryReplacementDiff + 1;
|
|
147
|
+
// we don't replace the opening " in the regex - that just makes sure
|
|
148
|
+
// we only find our marker at the start of a string value.
|
|
149
|
+
canaryReplacementDiff += m.length - 1;
|
|
150
|
+
for (const tagKey of Object.keys(metadata.strInfos[id].tags)) {
|
|
151
|
+
if (!tags[tagKey]) {
|
|
152
|
+
tags[tagKey] = [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const tagIdx of metadata.strInfos[id].tags[tagKey]) {
|
|
156
|
+
tags[tagKey].push(tagIdx + offset);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
metadata.history.add(metadata.strInfos[id]);
|
|
161
|
+
// replace the canary with the starting quote
|
|
162
|
+
return '"';
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
regex,
|
|
168
|
+
identity,
|
|
169
|
+
patchReplacer,
|
|
170
|
+
getUntrustedSpaceProps,
|
|
171
|
+
createSpaceTagRanges,
|
|
172
|
+
markCanaryValue,
|
|
173
|
+
applyCanaryTagRanges,
|
|
174
|
+
};
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const { kComponentName, ComponentBase } = require('@contrast/core
|
|
18
|
+
const { kComponentName, ComponentBase } = require('@contrast/core');
|
|
19
19
|
const { isString, set, traverseValues, primordials } = require('@contrast/common');
|
|
20
20
|
const { createFullLengthCopyTags } = require('../../tag-utils');
|
|
21
21
|
const { patchType } = require('../common');
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
const {
|
|
19
19
|
primordials: { StringPrototypeSubstr },
|
|
20
20
|
DataflowTag: { CUSTOM_VALIDATED_REFLECTED_XSS } } = require('@contrast/common');
|
|
21
|
-
const { kComponentName, ComponentBase } = require('@contrast/core
|
|
21
|
+
const { kComponentName, ComponentBase } = require('@contrast/core');
|
|
22
22
|
const { patchType } = require('../common');
|
|
23
23
|
|
|
24
24
|
const COMPONENT_NAME = 'assess.dataflow.propagation.sanitizeHtml';
|
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const { patchType } = require('../../common');
|
|
19
|
+
const { createSubsetTags, createAppendTags } = require('../../../tag-utils');
|
|
19
20
|
|
|
20
|
-
const {
|
|
21
|
+
const {
|
|
22
|
+
primordials: { StringPrototypeSplit }
|
|
23
|
+
} = require('@contrast/common');
|
|
21
24
|
|
|
22
25
|
module.exports = function(core) {
|
|
23
26
|
const {
|
|
@@ -49,7 +52,12 @@ module.exports = function(core) {
|
|
|
49
52
|
]
|
|
50
53
|
];
|
|
51
54
|
|
|
52
|
-
function
|
|
55
|
+
function calculateSubsetRange(tags, start, len) {
|
|
56
|
+
const clampedStart = Math.max(start, 0);
|
|
57
|
+
return createSubsetTags(tags, clampedStart, start + len - clampedStart);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getPropagationEvent(url, tags, data) {
|
|
53
61
|
const args = data.args.map((v) => {
|
|
54
62
|
const strInfo = tracker.getData(v);
|
|
55
63
|
return {
|
|
@@ -63,7 +71,7 @@ module.exports = function(core) {
|
|
|
63
71
|
moduleName: 'url',
|
|
64
72
|
methodName: 'URL',
|
|
65
73
|
get context() {
|
|
66
|
-
return `url.URL('${
|
|
74
|
+
return `url.URL('${url}')`;
|
|
67
75
|
},
|
|
68
76
|
object: {
|
|
69
77
|
value: 'url',
|
|
@@ -74,8 +82,8 @@ module.exports = function(core) {
|
|
|
74
82
|
tracked: true
|
|
75
83
|
},
|
|
76
84
|
args,
|
|
77
|
-
tags
|
|
78
|
-
history: [
|
|
85
|
+
tags,
|
|
86
|
+
history: [{ tags, value: url }],
|
|
79
87
|
source: 'P',
|
|
80
88
|
target: 'R',
|
|
81
89
|
stacktraceOpts: {
|
|
@@ -98,28 +106,47 @@ module.exports = function(core) {
|
|
|
98
106
|
if (!result || !args[0] || !getPropagatorContext()) return;
|
|
99
107
|
|
|
100
108
|
const [input, basename] = args;
|
|
109
|
+
const inputInfo = tracker.getData(input);
|
|
110
|
+
const basenameInfo = tracker.getData(basename);
|
|
111
|
+
|
|
101
112
|
let url = input;
|
|
113
|
+
let tags = inputInfo?.tags ?? null;
|
|
114
|
+
|
|
102
115
|
if (basename && url !== result.toString()) {
|
|
103
116
|
const isAbsoluteURL = url.indexOf('://') > 0 || url.indexOf('//') === 0;
|
|
104
117
|
if (isAbsoluteURL) {
|
|
105
|
-
|
|
118
|
+
// input is split into chunks so any leading slashes on the input
|
|
119
|
+
// can be replaced with the basename's protocol when needed. Each
|
|
120
|
+
// chunk's tag range is calculated from its offset within `input`.
|
|
121
|
+
const splitUrl = StringPrototypeSplit.call(url, new RegExp('(?=//)', 'g'));
|
|
106
122
|
let endOfSlashes = splitUrl.length === 1 ? true : false;
|
|
107
|
-
|
|
123
|
+
const useInputProtocol = splitUrl[0] === result.protocol;
|
|
124
|
+
let newUrl = useInputProtocol ? splitUrl[0] : StringPrototypeSplit.call(basename, /(?<=:)/)[0];
|
|
125
|
+
let newTags = useInputProtocol
|
|
126
|
+
? createSubsetTags(inputInfo?.tags, 0, newUrl.length)
|
|
127
|
+
: createSubsetTags(basenameInfo?.tags, 0, newUrl.length);
|
|
128
|
+
|
|
129
|
+
let inputOffset = 0;
|
|
108
130
|
for (let i = 0; i < splitUrl.length; i++) {
|
|
131
|
+
const piece = splitUrl[i];
|
|
109
132
|
if (endOfSlashes) {
|
|
110
|
-
|
|
133
|
+
const pieceTags = createSubsetTags(inputInfo?.tags, inputOffset, piece.length);
|
|
134
|
+
newTags = createAppendTags(newTags, pieceTags, newUrl.length);
|
|
135
|
+
newUrl += piece;
|
|
111
136
|
}
|
|
112
|
-
if (
|
|
137
|
+
if (piece === '/' && splitUrl[i + 1] !== '/') {
|
|
113
138
|
endOfSlashes = true;
|
|
114
139
|
}
|
|
140
|
+
inputOffset += piece.length;
|
|
115
141
|
}
|
|
116
142
|
url = newUrl;
|
|
143
|
+
tags = newTags;
|
|
117
144
|
} else {
|
|
118
|
-
|
|
145
|
+
tags = createAppendTags(basenameInfo?.tags, inputInfo?.tags, basename.length);
|
|
146
|
+
url = basename + url;
|
|
119
147
|
}
|
|
120
148
|
}
|
|
121
|
-
|
|
122
|
-
if (!strInfo) return;
|
|
149
|
+
if (!tags) return;
|
|
123
150
|
|
|
124
151
|
const proxy = new Proxy(data.result, {
|
|
125
152
|
set(obj, prop, value) {
|
|
@@ -139,31 +166,38 @@ module.exports = function(core) {
|
|
|
139
166
|
}
|
|
140
167
|
});
|
|
141
168
|
|
|
142
|
-
const traverse = function(href,
|
|
169
|
+
const traverse = function(href, hrefTags, urlObj, keys, idx = 0) {
|
|
143
170
|
let substr = href;
|
|
171
|
+
let substrTags = hrefTags;
|
|
144
172
|
keys.forEach((key) => {
|
|
145
173
|
if (typeof key === 'string') {
|
|
146
|
-
const part =
|
|
174
|
+
const part = urlObj[key];
|
|
147
175
|
if (part !== 'null') {
|
|
176
|
+
let partTags;
|
|
148
177
|
|
|
149
178
|
if (key === 'origin') {
|
|
150
179
|
const [protocol, originWithoutProtocol] = StringPrototypeSplit.call(part, new RegExp('(?<=//)'));
|
|
151
180
|
const idx1 = href.indexOf(protocol);
|
|
152
181
|
const idx2 = href.indexOf(originWithoutProtocol, idx - 1);
|
|
153
|
-
|
|
182
|
+
partTags = createAppendTags(
|
|
183
|
+
calculateSubsetRange(hrefTags, idx1, protocol.length),
|
|
184
|
+
calculateSubsetRange(hrefTags, idx2, originWithoutProtocol.length),
|
|
185
|
+
protocol.length
|
|
186
|
+
);
|
|
154
187
|
} else {
|
|
155
188
|
const index = href.indexOf(part, idx - 1);
|
|
156
|
-
|
|
189
|
+
partTags = calculateSubsetRange(hrefTags, index, part.length);
|
|
157
190
|
idx += part.length;
|
|
158
191
|
}
|
|
159
192
|
|
|
160
|
-
|
|
161
|
-
|
|
193
|
+
substr = part;
|
|
194
|
+
substrTags = partTags;
|
|
195
|
+
|
|
196
|
+
if (!partTags) return;
|
|
162
197
|
|
|
163
|
-
const event = getPropagationEvent(
|
|
198
|
+
const event = getPropagationEvent(url, partTags, data);
|
|
164
199
|
if (!event) return;
|
|
165
200
|
|
|
166
|
-
Object.assign(partInfo, event);
|
|
167
201
|
const { extern } = tracker.track(part, event);
|
|
168
202
|
|
|
169
203
|
if (extern) {
|
|
@@ -171,12 +205,12 @@ module.exports = function(core) {
|
|
|
171
205
|
}
|
|
172
206
|
}
|
|
173
207
|
} else {
|
|
174
|
-
traverse(substr,
|
|
208
|
+
traverse(substr, substrTags, urlObj, key, 0);
|
|
175
209
|
}
|
|
176
210
|
});
|
|
177
211
|
};
|
|
178
212
|
|
|
179
|
-
traverse(url, result, keys, 0);
|
|
213
|
+
traverse(url, tags, result, keys, 0);
|
|
180
214
|
|
|
181
215
|
if (proxy.search) {
|
|
182
216
|
proxy._contrast_searchParams = new URLSearchParams(proxy.search);
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
const { isString, primordials: { StringPrototypeMatch } } = require('@contrast/common');
|
|
19
19
|
const { createAppendTags } = require('../../tag-utils');
|
|
20
20
|
const { patchType } = require('../common');
|
|
21
|
+
const { regex, markCanaryValue, applyCanaryTagRanges } = require('./JSON/tag-ranges');
|
|
21
22
|
|
|
22
23
|
module.exports = function(core) {
|
|
23
24
|
const {
|
|
24
25
|
patcher,
|
|
26
|
+
scopes,
|
|
25
27
|
depHooks,
|
|
26
28
|
assess: {
|
|
27
29
|
inspect,
|
|
@@ -53,6 +55,32 @@ module.exports = function(core) {
|
|
|
53
55
|
return tags;
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
function stringifyWithTags(value) {
|
|
59
|
+
const strInfos = {};
|
|
60
|
+
let propagate = false;
|
|
61
|
+
|
|
62
|
+
function markCanaries(_key, val) {
|
|
63
|
+
const marked = markCanaryValue(tracker, strInfos, val, val);
|
|
64
|
+
if (marked.propagate) propagate = true;
|
|
65
|
+
return marked.value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = scopes.instrumentation.run({ lock: true }, () => JSON.stringify(value, markCanaries));
|
|
69
|
+
if (!propagate || result === undefined) {
|
|
70
|
+
return { value: result, tags: null, history: [] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const tags = {};
|
|
74
|
+
const metadata = { regex, strInfos, history: new Set() };
|
|
75
|
+
const taggedResult = applyCanaryTagRanges(result, metadata, tags);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
value: taggedResult,
|
|
79
|
+
tags: Object.keys(tags).length ? tags : null,
|
|
80
|
+
history: Array.from(metadata.history),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
return core.assess.dataflow.propagation.utilFormat = {
|
|
57
85
|
install() {
|
|
58
86
|
depHooks.resolve({ name: 'util', version: '*' }, (util) => {
|
|
@@ -97,9 +125,21 @@ module.exports = function(core) {
|
|
|
97
125
|
case 'f':
|
|
98
126
|
// won't be tracked
|
|
99
127
|
break;
|
|
100
|
-
case 'j':
|
|
101
|
-
|
|
102
|
-
|
|
128
|
+
case 'j': {
|
|
129
|
+
const stringified = stringifyWithTags(arg);
|
|
130
|
+
arg = stringified.value ?? 'undefined';
|
|
131
|
+
|
|
132
|
+
if (!stringified.tags) break;
|
|
133
|
+
|
|
134
|
+
const currIdx = result.indexOf(arg, idx);
|
|
135
|
+
if (currIdx === -1) break;
|
|
136
|
+
|
|
137
|
+
idx = currIdx + arg.length;
|
|
138
|
+
newTags = createAppendTags(newTags, stringified.tags, currIdx);
|
|
139
|
+
stringified.history.forEach((info) => history.push({ ...info }));
|
|
140
|
+
eventArgs.push({ value: arg, tracked: true });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
103
143
|
case 'o':
|
|
104
144
|
break; // handled below
|
|
105
145
|
case 'O':
|