@contrast/assess 1.24.1 → 1.25.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/JSON/parse-fn.js +36 -13
- package/lib/dataflow/propagation/install/JSON/parse.js +21 -51
- package/lib/dataflow/sinks/index.js +1 -0
- package/lib/dataflow/sinks/install/hapi/index.js +30 -0
- package/lib/dataflow/sinks/install/hapi/unvalidated-redirect.js +139 -0
- package/lib/session-configuration/install/hapi.js +1 -1
- package/package.json +2 -2
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
15
|
'use strict';
|
|
16
|
+
|
|
16
17
|
const { trim } = require('@contrast/common');
|
|
17
18
|
|
|
18
19
|
function isNumber(value) {
|
|
@@ -200,25 +201,48 @@ function getValueIndexes(value, index, accumulator) {
|
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
/**
|
|
205
|
+
* JSON strings can have leading and trailing whitespace characters. This will return
|
|
206
|
+
* the start and end indices of the input's characters that represent actual JSON data.
|
|
207
|
+
* Examples:
|
|
208
|
+
* `"hi"` => [0, 3]
|
|
209
|
+
* ` "hi"\n` => [1, 4]
|
|
210
|
+
* @param {string} input raw JSON value being parsed
|
|
211
|
+
* @returns {number[]}
|
|
212
|
+
*/
|
|
213
|
+
function getStartEndIndices(input) {
|
|
214
|
+
let startCharIdx = 0;
|
|
215
|
+
let endCharIdx = input.length - 1;
|
|
216
|
+
|
|
217
|
+
while (!trim(input[startCharIdx])) {
|
|
218
|
+
startCharIdx++;
|
|
219
|
+
}
|
|
220
|
+
while (!trim(input[endCharIdx])) {
|
|
221
|
+
endCharIdx--;
|
|
222
|
+
}
|
|
223
|
+
return [startCharIdx, endCharIdx];
|
|
224
|
+
}
|
|
225
|
+
|
|
203
226
|
function processInput(input) {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
227
|
+
const [startIdx, endIdx] = getStartEndIndices(input);
|
|
228
|
+
const firstChar = input[startIdx];
|
|
229
|
+
const lastChar = input[endIdx];
|
|
206
230
|
const accumulator = [];
|
|
207
231
|
|
|
208
|
-
if (
|
|
209
|
-
return object(input, 1, accumulator);
|
|
232
|
+
if (firstChar === '{' && lastChar === '}') {
|
|
233
|
+
return object(input, startIdx + 1, accumulator);
|
|
210
234
|
}
|
|
211
235
|
|
|
212
|
-
if (
|
|
213
|
-
return array(input, 1, accumulator);
|
|
236
|
+
if (firstChar === '[' && lastChar === ']') {
|
|
237
|
+
return array(input, startIdx + 1, accumulator);
|
|
214
238
|
}
|
|
215
239
|
|
|
216
240
|
if (isNumber(input)) {
|
|
217
|
-
return number(input,
|
|
241
|
+
return number(input, startIdx);
|
|
218
242
|
}
|
|
219
243
|
|
|
220
|
-
if (
|
|
221
|
-
return string(input, 1);
|
|
244
|
+
if (firstChar === '"' && lastChar === '"') {
|
|
245
|
+
return string(input, startIdx + 1);
|
|
222
246
|
}
|
|
223
247
|
|
|
224
248
|
switch (input) {
|
|
@@ -228,21 +252,20 @@ function processInput(input) {
|
|
|
228
252
|
case 'null':
|
|
229
253
|
return nully();
|
|
230
254
|
default:
|
|
231
|
-
return string(input,
|
|
255
|
+
return string(input, startIdx);
|
|
232
256
|
}
|
|
233
257
|
}
|
|
234
258
|
|
|
235
259
|
function wrapEndResult({ accumulator, startIndex, endIndex }) {
|
|
236
260
|
accumulator = accumulator || [];
|
|
237
261
|
accumulator.push({ key: '', value: [startIndex, endIndex] });
|
|
238
|
-
|
|
239
262
|
return accumulator;
|
|
240
263
|
}
|
|
241
264
|
|
|
242
|
-
function
|
|
265
|
+
function getKeyValueIndices(input) {
|
|
243
266
|
return wrapEndResult(processInput(input));
|
|
244
267
|
}
|
|
245
268
|
|
|
246
269
|
module.exports = {
|
|
247
|
-
|
|
270
|
+
getKeyValueIndices,
|
|
248
271
|
};
|
|
@@ -16,11 +16,9 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const { isString, inspect } = require('@contrast/common');
|
|
19
|
+
const { createSubsetTags } = require('../../../tag-utils');
|
|
19
20
|
const { patchType } = require('../../common');
|
|
20
|
-
const {
|
|
21
|
-
const {
|
|
22
|
-
createOverlappingTags
|
|
23
|
-
} = require('../../../tag-utils');
|
|
21
|
+
const { getKeyValueIndices } = require('./parse-fn');
|
|
24
22
|
|
|
25
23
|
/*
|
|
26
24
|
When we return a string as a result of a reviver call
|
|
@@ -69,6 +67,7 @@ module.exports = function (core) {
|
|
|
69
67
|
tracked: false,
|
|
70
68
|
}
|
|
71
69
|
].filter(Boolean);
|
|
70
|
+
|
|
72
71
|
return createPropagationEvent({
|
|
73
72
|
context: `${method}(${eventArgs.map((arg) => `'${arg.value}'`)})`,
|
|
74
73
|
name: method,
|
|
@@ -95,37 +94,7 @@ module.exports = function (core) {
|
|
|
95
94
|
});
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
|
|
99
|
-
const overlappingRanges = createOverlappingTags(
|
|
100
|
-
strInfo.tags,
|
|
101
|
-
startIndex,
|
|
102
|
-
endIndex
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
const tags = {};
|
|
106
|
-
const startingOffset = startIndex;
|
|
107
|
-
const normalizedEndIndex = endIndex - startingOffset;
|
|
108
|
-
|
|
109
|
-
Object.entries(overlappingRanges).forEach(([tag, ranges]) => {
|
|
110
|
-
tags[tag] = [];
|
|
111
|
-
|
|
112
|
-
ranges.forEach(([start, end]) => {
|
|
113
|
-
const transferredStartIndex = start - startingOffset;
|
|
114
|
-
const transferredEndIndex = end - startingOffset;
|
|
115
|
-
|
|
116
|
-
tags[tag].push(transferredStartIndex < 0 ? 0 : transferredStartIndex);
|
|
117
|
-
tags[tag].push(
|
|
118
|
-
transferredEndIndex > normalizedEndIndex
|
|
119
|
-
? normalizedEndIndex
|
|
120
|
-
: transferredEndIndex
|
|
121
|
-
);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
return tags;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return (core.assess.dataflow.propagation.jsonInstrumentation.parse = {
|
|
97
|
+
return core.assess.dataflow.propagation.jsonInstrumentation.parse = {
|
|
129
98
|
install() {
|
|
130
99
|
patcher.patch(JSON, 'parse', {
|
|
131
100
|
name: 'JSON.prototype.parse',
|
|
@@ -138,36 +107,37 @@ module.exports = function (core) {
|
|
|
138
107
|
if (!strInfo) return;
|
|
139
108
|
|
|
140
109
|
const stack = [];
|
|
141
|
-
let
|
|
110
|
+
let keyValueIndices = [];
|
|
142
111
|
|
|
143
112
|
try {
|
|
144
|
-
|
|
113
|
+
keyValueIndices = getKeyValueIndices(input);
|
|
145
114
|
} catch (err) {
|
|
146
115
|
logger.warn({ err, funcKey: data.funcKey, string: input }, 'JSON.parse() propagation failed');
|
|
147
116
|
}
|
|
148
117
|
|
|
149
|
-
if (
|
|
118
|
+
if (keyValueIndices.length === 0) return;
|
|
150
119
|
|
|
151
120
|
let i = 0;
|
|
152
|
-
function contrastParseReviver(key, value) {
|
|
153
|
-
const { value: [startIndex, endIndex] } = keyValueIndexes[i];
|
|
154
121
|
|
|
155
|
-
|
|
122
|
+
function contrastParseReviver(key, value) {
|
|
123
|
+
// if keyValueIndices[i] doesn't exist then getKeyValueIndexes() returned incorrect data
|
|
124
|
+
if (!value || !isString(value) || !keyValueIndices[i]) {
|
|
156
125
|
return reviver ? reviver(key, value) : value;
|
|
157
126
|
}
|
|
158
127
|
|
|
159
|
-
const
|
|
128
|
+
const { value: [startIdx, endIdx] } = keyValueIndices[i];
|
|
129
|
+
const newTags = createSubsetTags(strInfo.tags, startIdx, endIdx - startIdx + 1);
|
|
130
|
+
if (newTags) {
|
|
131
|
+
const event = createEvent(data, value, newTags, reviver, strInfo);
|
|
132
|
+
if (!event || Object.keys(newTags).length === 0) {
|
|
133
|
+
return reviver ? reviver(key, value) : value;
|
|
134
|
+
}
|
|
160
135
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return reviver ? reviver(key, value) : value;
|
|
136
|
+
const { extern } = tracker.track(value, event);
|
|
137
|
+
if (extern) return reviver ? reviver(key, extern) : extern;
|
|
164
138
|
}
|
|
165
139
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (reviver) return reviver(key, extern);
|
|
169
|
-
|
|
170
|
-
return extern;
|
|
140
|
+
return reviver ? reviver(key, value) : value;
|
|
171
141
|
}
|
|
172
142
|
|
|
173
143
|
data.args[1] = function (key, value) {
|
|
@@ -188,5 +158,5 @@ module.exports = function (core) {
|
|
|
188
158
|
uninstall() {
|
|
189
159
|
JSON.parse = patcher.unwrap(JSON.parse);
|
|
190
160
|
},
|
|
191
|
-
}
|
|
161
|
+
};
|
|
192
162
|
};
|
|
@@ -68,6 +68,7 @@ module.exports = function (core) {
|
|
|
68
68
|
|
|
69
69
|
require('./install/express')(core);
|
|
70
70
|
require('./install/fastify')(core);
|
|
71
|
+
require('./install/hapi')(core);
|
|
71
72
|
require('./install/koa')(core);
|
|
72
73
|
require('./install/child-process')(core);
|
|
73
74
|
require('./install/eval')(core);
|
|
@@ -0,0 +1,30 @@
|
|
|
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 { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
|
+
|
|
20
|
+
module.exports = function(core) {
|
|
21
|
+
const hapi = core.assess.dataflow.sinks.hapi = {};
|
|
22
|
+
|
|
23
|
+
require('./unvalidated-redirect')(core);
|
|
24
|
+
|
|
25
|
+
hapi.install = function() {
|
|
26
|
+
callChildComponentMethodsSync(hapi, 'install');
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return hapi;
|
|
30
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
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: { UNVALIDATED_REDIRECT: ruleId },
|
|
21
|
+
DataflowTag: {
|
|
22
|
+
UNTRUSTED,
|
|
23
|
+
CUSTOM_ENCODED,
|
|
24
|
+
CUSTOM_VALIDATED,
|
|
25
|
+
HTML_ENCODED,
|
|
26
|
+
LIMITED_CHARS,
|
|
27
|
+
URL_ENCODED,
|
|
28
|
+
},
|
|
29
|
+
isString
|
|
30
|
+
} = require('@contrast/common');
|
|
31
|
+
const { InstrumentationType: { RULE } } = require('../../../../constants');
|
|
32
|
+
const { patchType, filterSafeTags } = require('../../common');
|
|
33
|
+
const { createSubsetTags } = require('../../../tag-utils');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {{
|
|
37
|
+
* assess: import('@contrast/assess').Assess,
|
|
38
|
+
* config: import('@contrast/config').Config,
|
|
39
|
+
* logger: import('@contrast/logger').Logger,
|
|
40
|
+
* }} core
|
|
41
|
+
* @returns {import('@contrast/common').Installable}
|
|
42
|
+
*/
|
|
43
|
+
module.exports = function(core) {
|
|
44
|
+
const {
|
|
45
|
+
depHooks,
|
|
46
|
+
patcher,
|
|
47
|
+
config,
|
|
48
|
+
assess: {
|
|
49
|
+
getSourceContext,
|
|
50
|
+
eventFactory: { createSinkEvent },
|
|
51
|
+
dataflow: {
|
|
52
|
+
tracker,
|
|
53
|
+
sinks: { isVulnerable, reportFindings, reportSafePositive }
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
} = core;
|
|
57
|
+
|
|
58
|
+
const unvalidatedRedirect = core.assess.dataflow.sinks.hapi.unvalidatedRedirect = {};
|
|
59
|
+
const inspect = patcher.unwrap(util.inspect);
|
|
60
|
+
|
|
61
|
+
const safeTags = [
|
|
62
|
+
CUSTOM_ENCODED,
|
|
63
|
+
CUSTOM_VALIDATED,
|
|
64
|
+
HTML_ENCODED,
|
|
65
|
+
LIMITED_CHARS,
|
|
66
|
+
URL_ENCODED,
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
unvalidatedRedirect.install = function() {
|
|
70
|
+
depHooks.resolve({ name: '@hapi/hapi', file: 'lib/response' }, (Response) => {
|
|
71
|
+
const name = 'hapi.Response.prototype.redirect';
|
|
72
|
+
patcher.patch(Response.prototype, 'redirect', {
|
|
73
|
+
name,
|
|
74
|
+
patchType,
|
|
75
|
+
pre: (data) => {
|
|
76
|
+
if (!getSourceContext(RULE, ruleId)) return;
|
|
77
|
+
|
|
78
|
+
const [url] = data.args;
|
|
79
|
+
if (!url || !isString(url)) return;
|
|
80
|
+
|
|
81
|
+
const strInfo = tracker.getData(url);
|
|
82
|
+
if (!strInfo) return;
|
|
83
|
+
|
|
84
|
+
let urlPathTags = strInfo.tags;
|
|
85
|
+
if (url.indexOf('?') > -1) {
|
|
86
|
+
urlPathTags = createSubsetTags(strInfo.tags, 0, url.indexOf('?') + 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (urlPathTags && isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
|
|
90
|
+
const event = createSinkEvent({
|
|
91
|
+
args: [{
|
|
92
|
+
tracked: true,
|
|
93
|
+
value: strInfo.value,
|
|
94
|
+
}],
|
|
95
|
+
context: `response.redirect(${inspect(strInfo.value)})`,
|
|
96
|
+
history: [strInfo],
|
|
97
|
+
name,
|
|
98
|
+
moduleName: 'hapi',
|
|
99
|
+
methodName: 'Response.prototype.redirect',
|
|
100
|
+
object: {
|
|
101
|
+
tracked: false,
|
|
102
|
+
value: 'hapi.Response',
|
|
103
|
+
},
|
|
104
|
+
result: {
|
|
105
|
+
tracked: false,
|
|
106
|
+
value: undefined,
|
|
107
|
+
},
|
|
108
|
+
tags: urlPathTags,
|
|
109
|
+
source: 'P0',
|
|
110
|
+
stacktraceOpts: {
|
|
111
|
+
constructorOpt: data.hooked,
|
|
112
|
+
prependFrames: [data.orig]
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (event) {
|
|
117
|
+
reportFindings({
|
|
118
|
+
ruleId,
|
|
119
|
+
sinkEvent: event,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
} else if (config.assess.safe_positives.enable) {
|
|
123
|
+
reportSafePositive({
|
|
124
|
+
name,
|
|
125
|
+
ruleId,
|
|
126
|
+
safeTags: filterSafeTags(safeTags, strInfo),
|
|
127
|
+
strInfo: {
|
|
128
|
+
tags: strInfo.tags,
|
|
129
|
+
value: strInfo.value,
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return unvalidatedRedirect;
|
|
139
|
+
};
|
|
@@ -36,7 +36,7 @@ module.exports = function (core) {
|
|
|
36
36
|
const inspect = patcher.unwrap(util.inspect);
|
|
37
37
|
|
|
38
38
|
hapiSession.install = function () {
|
|
39
|
-
return depHooks.resolve({ name: '@hapi/hapi', version: '>=18 <
|
|
39
|
+
return depHooks.resolve({ name: '@hapi/hapi', version: '>=18 <22' }, (hapi) => {
|
|
40
40
|
['server', 'Server'].forEach((server) => {
|
|
41
41
|
patcher.patch(hapi, server, {
|
|
42
42
|
name: `hapi.${server}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/assess",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.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.19.0",
|
|
21
21
|
"@contrast/distringuish": "^4.4.0",
|
|
22
22
|
"@contrast/scopes": "1.4.0"
|
|
23
23
|
}
|