@contrast/assess 1.27.0 → 1.27.2
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/sinks/install/child-process.js +1 -1
- package/lib/dataflow/sinks/install/eval.js +1 -0
- package/lib/dataflow/sinks/install/express/reflected-xss.js +1 -0
- package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +1 -0
- package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +1 -0
- package/lib/dataflow/sinks/install/fs.js +2 -0
- package/lib/dataflow/sinks/install/function.js +1 -0
- package/lib/dataflow/sinks/install/hapi/unvalidated-redirect.js +1 -0
- package/lib/dataflow/sinks/install/http/request.js +1 -0
- package/lib/dataflow/sinks/install/http/server-response.js +1 -0
- package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +1 -0
- package/lib/dataflow/sinks/install/libxmljs.js +1 -0
- package/lib/dataflow/sinks/install/marsdb.js +1 -0
- package/lib/dataflow/sinks/install/mongodb.js +1 -0
- package/lib/dataflow/sinks/install/mssql.js +1 -0
- package/lib/dataflow/sinks/install/mysql.js +1 -0
- package/lib/dataflow/sinks/install/node-serialize.js +3 -1
- package/lib/dataflow/sinks/install/postgres.js +1 -0
- package/lib/dataflow/sinks/install/sequelize.js +1 -0
- package/lib/dataflow/sinks/install/sqlite3.js +1 -0
- package/lib/dataflow/sources/handler.js +34 -12
- package/lib/dataflow/sources/install/http.js +58 -4
- package/lib/get-policy.js +256 -92
- package/lib/get-source-context.js +1 -1
- package/package.json +4 -4
|
@@ -31,6 +31,7 @@ const { InstrumentationType: { RULE } } = require('../../../constants');
|
|
|
31
31
|
const { patchType, filterSafeTags } = require('../common');
|
|
32
32
|
|
|
33
33
|
const safeTags = [
|
|
34
|
+
`excluded:${ruleId}`,
|
|
34
35
|
CUSTOM_ENCODED_TRUST_BOUNDARY_VIOLATION,
|
|
35
36
|
CUSTOM_ENCODED,
|
|
36
37
|
CUSTOM_VALIDATED_TRUST_BOUNDARY_VIOLATION,
|
|
@@ -46,6 +46,7 @@ module.exports = function(core) {
|
|
|
46
46
|
} = core;
|
|
47
47
|
|
|
48
48
|
const safeTags = [
|
|
49
|
+
`excluded:${ruleId}`,
|
|
49
50
|
URL_ENCODED,
|
|
50
51
|
LIMITED_CHARS,
|
|
51
52
|
ALPHANUM_SPACE_HYPHEN,
|
|
@@ -77,6 +78,7 @@ module.exports = function(core) {
|
|
|
77
78
|
});
|
|
78
79
|
for (let i = 0; i < values.length; i++) {
|
|
79
80
|
const { strInfo } = args[i];
|
|
81
|
+
|
|
80
82
|
if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
|
|
81
83
|
continue;
|
|
82
84
|
}
|
|
@@ -33,6 +33,7 @@ const { InstrumentationType: { RULE } } = require('../../../constants');
|
|
|
33
33
|
const { patchType, filterSafeTags } = require('../common');
|
|
34
34
|
|
|
35
35
|
const safeTags = [
|
|
36
|
+
`excluded:${ruleId}`,
|
|
36
37
|
CUSTOM_ENCODED_TRUST_BOUNDARY_VIOLATION,
|
|
37
38
|
CUSTOM_ENCODED,
|
|
38
39
|
CUSTOM_VALIDATED_TRUST_BOUNDARY_VIOLATION,
|
|
@@ -25,6 +25,8 @@ const {
|
|
|
25
25
|
const { InstrumentationType: { RULE } } = require('../../../constants');
|
|
26
26
|
const { patchType } = require('../common');
|
|
27
27
|
|
|
28
|
+
const safeTags = [`excluded:${ruleId}`];
|
|
29
|
+
|
|
28
30
|
/**
|
|
29
31
|
* @param {{
|
|
30
32
|
* assess: import('@contrast/assess').Assess,
|
|
@@ -59,7 +61,7 @@ module.exports = function(core) {
|
|
|
59
61
|
if (!isString(input)) return;
|
|
60
62
|
|
|
61
63
|
const strInfo = tracker.getData(input);
|
|
62
|
-
if (!strInfo || !isVulnerable(UNTRUSTED,
|
|
64
|
+
if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) return;
|
|
63
65
|
|
|
64
66
|
const sinkEvent = createSinkEvent({
|
|
65
67
|
name: 'node-serialize.unserialize',
|
|
@@ -39,7 +39,7 @@ module.exports = function (core) {
|
|
|
39
39
|
|
|
40
40
|
const emptyStack = Object.freeze([]);
|
|
41
41
|
|
|
42
|
-
sources.createTags = function createTags({ inputType, fieldName = '', value }) {
|
|
42
|
+
sources.createTags = function createTags({ inputType, fieldName = '', value, tagNames }) {
|
|
43
43
|
if (!value?.length) {
|
|
44
44
|
return null;
|
|
45
45
|
}
|
|
@@ -49,6 +49,12 @@ module.exports = function (core) {
|
|
|
49
49
|
[DataflowTag.UNTRUSTED]: [0, stop]
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
if (tagNames) {
|
|
53
|
+
for (const tag of tagNames) {
|
|
54
|
+
tags[tag] = [0, stop];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
if (inputType === InputType.HEADER && fieldName.toLowerCase() === 'referer') {
|
|
53
59
|
tags[DataflowTag.HEADER] = [0, stop];
|
|
54
60
|
}
|
|
@@ -78,10 +84,16 @@ module.exports = function (core) {
|
|
|
78
84
|
return null;
|
|
79
85
|
}
|
|
80
86
|
|
|
87
|
+
// url exclusion
|
|
88
|
+
if (!sourceContext.policy) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
if (!context) {
|
|
82
93
|
context = inputType;
|
|
83
94
|
}
|
|
84
95
|
|
|
96
|
+
const { policy: requestPolicy } = sourceContext;
|
|
85
97
|
const max = config.assess.max_context_source_events;
|
|
86
98
|
let _data = data;
|
|
87
99
|
let stack;
|
|
@@ -93,7 +105,8 @@ module.exports = function (core) {
|
|
|
93
105
|
}
|
|
94
106
|
}
|
|
95
107
|
|
|
96
|
-
function createEvent({ fieldName, pathName, value }) {
|
|
108
|
+
function createEvent({ fieldName, pathName, value, excludedRules }) {
|
|
109
|
+
const tagNames = Array.from(excludedRules).map((ruleId) => `excluded:${ruleId}`);
|
|
97
110
|
// create the stacktrace once per call to .handle()
|
|
98
111
|
stack || (stack = sources.createStacktrace(stacktraceOpts));
|
|
99
112
|
return eventFactory.createSourceEvent({
|
|
@@ -103,16 +116,23 @@ module.exports = function (core) {
|
|
|
103
116
|
pathName,
|
|
104
117
|
stack,
|
|
105
118
|
inputType,
|
|
106
|
-
tags: sources.createTags({ inputType, fieldName, value }),
|
|
119
|
+
tags: sources.createTags({ inputType, fieldName, value, tagNames }),
|
|
107
120
|
result: { tracked: true, value },
|
|
108
121
|
});
|
|
109
122
|
}
|
|
110
123
|
|
|
111
124
|
if (Buffer.isBuffer(data) && !tracker.getData(data)) {
|
|
112
|
-
const
|
|
125
|
+
const { track, excludedRules } = requestPolicy.getInputPolicy(InputType.BODY);
|
|
126
|
+
if (!track) {
|
|
127
|
+
core.logger.debug({ inputType }, 'assess input exclusion disabled tracking');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const event = createEvent({ pathName: 'body', value: data, fieldName: '', excludedRules });
|
|
113
132
|
if (event) {
|
|
114
133
|
tracker.track(data, event);
|
|
115
134
|
}
|
|
135
|
+
|
|
116
136
|
return;
|
|
117
137
|
}
|
|
118
138
|
|
|
@@ -124,18 +144,22 @@ module.exports = function (core) {
|
|
|
124
144
|
return true;
|
|
125
145
|
}
|
|
126
146
|
|
|
147
|
+
const { track, excludedRules } = sourceContext.policy.getInputPolicy(inputType, fieldName);
|
|
148
|
+
if (!track) {
|
|
149
|
+
core.logger.debug({ fieldName, inputType }, 'assess input exclusion disabling tracking');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
127
153
|
if (isString(value) && value.length) {
|
|
128
154
|
const strInfo = tracker.getData(value);
|
|
129
|
-
|
|
130
155
|
if (strInfo) {
|
|
131
156
|
// TODO: confirm this "layering-on" approach is what we want
|
|
132
|
-
// when
|
|
157
|
+
// when a value is already tracked, the handler will "re-track" the value with new source
|
|
133
158
|
// event metadata. without this step tracker would complain about value already being tracked.
|
|
134
159
|
// alternatively we could treat this more like a propagation event and update existing metadata.
|
|
135
160
|
value = strInfo.value;
|
|
136
161
|
}
|
|
137
|
-
|
|
138
|
-
const event = createEvent({ pathName, value, fieldName });
|
|
162
|
+
const event = createEvent({ pathName, value, fieldName, excludedRules });
|
|
139
163
|
if (!event) {
|
|
140
164
|
logger.warn({ inputType, sourceName: name, pathName, value }, 'unable to create source event');
|
|
141
165
|
return;
|
|
@@ -149,7 +173,7 @@ module.exports = function (core) {
|
|
|
149
173
|
sourceContext.sourceEventsCount++;
|
|
150
174
|
}
|
|
151
175
|
} else if (Buffer.isBuffer(value) && !tracker.getData(value)) {
|
|
152
|
-
const event = createEvent({ pathName, value, fieldName });
|
|
176
|
+
const event = createEvent({ pathName, value, fieldName, excludedRules });
|
|
153
177
|
if (event) {
|
|
154
178
|
tracker.track(value, event);
|
|
155
179
|
} else {
|
|
@@ -193,9 +217,7 @@ function traverse(target, cb, path = [], visited = new Set()) {
|
|
|
193
217
|
|
|
194
218
|
if (isVisitable(value)) {
|
|
195
219
|
const halt = cb(path, key, value, target) === false;
|
|
196
|
-
if (halt)
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
220
|
+
if (halt) return;
|
|
199
221
|
}
|
|
200
222
|
|
|
201
223
|
if (isTraversable(value)) {
|
|
@@ -102,6 +102,21 @@ module.exports = function (core) {
|
|
|
102
102
|
sourceContext: store.assess
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
+
// track the headers and the url.
|
|
106
|
+
//
|
|
107
|
+
// note that req.headers and req.headersDistinct are now (as of v15.1.0)
|
|
108
|
+
// lazily computed using an accessor property.
|
|
109
|
+
//
|
|
110
|
+
// there is no need to track headersDistinct because they are not
|
|
111
|
+
// referenced prior to this point. and, when they are referenced, node
|
|
112
|
+
// populates them with references to the (what will be after code below)
|
|
113
|
+
// already-tracked values in rawHeaders. But headers have already been
|
|
114
|
+
// referenced by node before the 'request' event is emitted by the server,
|
|
115
|
+
// so headers need to be tracked independently of rawHeaders. The way
|
|
116
|
+
// node handles the headers is convoluted; it's easier/safer to track the
|
|
117
|
+
// headers as they are. An attacker could use knowledge of node's handling
|
|
118
|
+
// to craft their attack.
|
|
119
|
+
//
|
|
105
120
|
[
|
|
106
121
|
{
|
|
107
122
|
context: 'req.headers',
|
|
@@ -117,18 +132,57 @@ module.exports = function (core) {
|
|
|
117
132
|
...sourceInfo,
|
|
118
133
|
}
|
|
119
134
|
].forEach((sourceData) => {
|
|
120
|
-
const { inputType } = sourceData;
|
|
121
135
|
try {
|
|
122
136
|
dataflow.sources.handle(sourceData);
|
|
123
137
|
} catch (err) {
|
|
138
|
+
const { inputType } = sourceData;
|
|
124
139
|
logger.error({ err, inputType, sourceName }, 'unable to handle http source');
|
|
125
140
|
}
|
|
126
141
|
});
|
|
127
142
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
|
|
144
|
+
//
|
|
145
|
+
// now track the rawHeaders. headers are complicated because they appear
|
|
146
|
+
// three times: headers, headersDistinct, and rawHeaders and we want to
|
|
147
|
+
// create only one event per header value. that turns out not to be as
|
|
148
|
+
// easy/possible as it sounds, due to the way node handles req.headers.
|
|
149
|
+
//
|
|
150
|
+
// see node's lib/_http_incoming.js for details. interesting optimizations
|
|
151
|
+
// and quirky handling per the RFC. some duplicate headers are joined by
|
|
152
|
+
// default, some are not.
|
|
153
|
+
//
|
|
154
|
+
// but we have to track rawHeaders. they are copied to a separate array
|
|
155
|
+
// because the dataflow.sources.handle() doesn't know about an array where
|
|
156
|
+
// only odd indexes are to be tracked.
|
|
157
|
+
//
|
|
158
|
+
// even though we could track the rawHeaders' keys, we don't because they
|
|
159
|
+
// are not used by any application that i'm aware of. it's easy enough to
|
|
160
|
+
// add here if we find there is an edge case where the application bypasses
|
|
161
|
+
// headers and headersDistinct and uses rawHeaders directly.
|
|
162
|
+
//
|
|
163
|
+
const headerValues = [];
|
|
164
|
+
for (let i = 1; i < req.rawHeaders.length; i += 2) {
|
|
165
|
+
headerValues.push(req.rawHeaders[i]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
dataflow.sources.handle({
|
|
170
|
+
context: 'req.headers',
|
|
171
|
+
inputType: InputType.HEADER,
|
|
172
|
+
data: headerValues,
|
|
173
|
+
...sourceInfo,
|
|
174
|
+
});
|
|
175
|
+
} catch (err) {
|
|
176
|
+
logger.error({ err, inputType: InputType.HEADER, sourceName }, 'unable to handle http source');
|
|
131
177
|
}
|
|
178
|
+
|
|
179
|
+
//
|
|
180
|
+
// now that the raw headers are tracked, put each tracked value back
|
|
181
|
+
//
|
|
182
|
+
for (let i = 0; i < headerValues.length; i++) {
|
|
183
|
+
req.rawHeaders[(i << 1) + 1] = headerValues[i];
|
|
184
|
+
}
|
|
185
|
+
|
|
132
186
|
} catch (err) {
|
|
133
187
|
logger.error({ err, funcKey: data.funcKey }, 'Error during Assess request handling');
|
|
134
188
|
}
|
package/lib/get-policy.js
CHANGED
|
@@ -16,51 +16,39 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const {
|
|
19
|
+
Event,
|
|
20
|
+
ExclusionType,
|
|
21
|
+
InputType,
|
|
19
22
|
Rule,
|
|
20
23
|
ResponseScanningRule,
|
|
21
24
|
SessionConfigurationRule,
|
|
22
|
-
Event,
|
|
23
25
|
join,
|
|
24
|
-
toLowerCase,
|
|
25
26
|
} = require('@contrast/common');
|
|
26
27
|
|
|
27
|
-
const
|
|
28
|
+
const ASSESS_RULES = Object.values({
|
|
28
29
|
...Rule,
|
|
29
30
|
...ResponseScanningRule,
|
|
30
31
|
...SessionConfigurationRule,
|
|
31
32
|
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return buildUriPathRegExp(urls);
|
|
52
|
-
} else {
|
|
53
|
-
return () => true;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function regExpCheck(str) {
|
|
58
|
-
return str.indexOf('*') > 0 ||
|
|
59
|
-
str.indexOf('.') > 0 ||
|
|
60
|
-
str.indexOf('+') > 0 ||
|
|
61
|
-
str.indexOf('?') > 0 ||
|
|
62
|
-
str.indexOf('\\') > 0;
|
|
63
|
-
}
|
|
33
|
+
const BROAD_INPUT_EXCLUSION_TYPES = [
|
|
34
|
+
ExclusionType.BODY,
|
|
35
|
+
ExclusionType.QUERYSTRING
|
|
36
|
+
];
|
|
37
|
+
const NAMED_INPUT_EXCLUSION_TYPES = [
|
|
38
|
+
ExclusionType.COOKIE,
|
|
39
|
+
ExclusionType.HEADER,
|
|
40
|
+
ExclusionType.PARAMETER
|
|
41
|
+
];
|
|
42
|
+
const BODY_TYPES = [
|
|
43
|
+
InputType.BODY,
|
|
44
|
+
InputType.JSON_VALUE,
|
|
45
|
+
InputType.JSON_ARRAYED_VALUE,
|
|
46
|
+
InputType.MULTIPART_CONTENT_TYPE,
|
|
47
|
+
InputType.MULTIPART_FIELD_NAME,
|
|
48
|
+
InputType.MULTIPART_NAME,
|
|
49
|
+
InputType.MULTIPART_VALUE,
|
|
50
|
+
];
|
|
51
|
+
const DISABLED_INPUT_POLICY = { track: false };
|
|
64
52
|
|
|
65
53
|
/**
|
|
66
54
|
* @param {{
|
|
@@ -73,100 +61,276 @@ function regExpCheck(str) {
|
|
|
73
61
|
module.exports = function assess(core) {
|
|
74
62
|
const { config, logger, messages } = core;
|
|
75
63
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
64
|
+
const globalPolicy = {
|
|
65
|
+
// by default all rules are enabled
|
|
66
|
+
enabledRules: new Set(ASSESS_RULES),
|
|
67
|
+
exclusionMap: new Map([
|
|
68
|
+
[ExclusionType.BODY, []],
|
|
69
|
+
[ExclusionType.COOKIE, []],
|
|
70
|
+
[ExclusionType.HEADER, []],
|
|
71
|
+
[ExclusionType.PARAMETER, []],
|
|
72
|
+
[ExclusionType.QUERYSTRING, []],
|
|
73
|
+
[ExclusionType.URL, []],
|
|
74
|
+
]),
|
|
79
75
|
};
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
exclusions[key] = [];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const rawDtmList = [
|
|
88
|
-
// todo: NODE-3281 input exclusions
|
|
89
|
-
...(settings?.exclusions?.url || [])
|
|
90
|
-
].filter((exclusion) => exclusion.modes.includes('assess'));
|
|
91
|
-
|
|
92
|
-
if (!rawDtmList.length) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
for (const dtm of rawDtmList) {
|
|
97
|
-
dtm.type = dtm.type || 'URL';
|
|
98
|
-
|
|
99
|
-
const { name, assess_rules, urls, type } = dtm;
|
|
100
|
-
const key = toLowerCase(type);
|
|
101
|
-
try {
|
|
102
|
-
const e = {
|
|
103
|
-
name,
|
|
104
|
-
rules: new Set(assess_rules),
|
|
105
|
-
};
|
|
106
|
-
e.matchesUriPath = createUriPathMatcher(urls);
|
|
107
|
-
exclusions[key].push(e);
|
|
108
|
-
} catch (err) {
|
|
109
|
-
logger.error({ err, dtm }, 'failed to process exclusion');
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Subscribe to settings updates and modify global policy accordingly.
|
|
79
|
+
*/
|
|
114
80
|
messages.on(Event.SERVER_SETTINGS_UPDATE, (msg) => {
|
|
115
81
|
if (!config.getEffectiveValue('assess.enable')) return;
|
|
116
82
|
|
|
117
83
|
if (msg.assess) {
|
|
118
|
-
for (const ruleId of
|
|
84
|
+
for (const ruleId of ASSESS_RULES) {
|
|
119
85
|
const enable = msg.assess[ruleId]?.enable;
|
|
120
86
|
if (enable === true) {
|
|
121
|
-
enabledRules.add(ruleId);
|
|
122
|
-
if (ruleId === Rule.NOSQL_INJECTION) enabledRules.add(Rule.NOSQL_INJECTION_MONGO);
|
|
87
|
+
globalPolicy.enabledRules.add(ruleId);
|
|
88
|
+
if (ruleId === Rule.NOSQL_INJECTION) globalPolicy.enabledRules.add(Rule.NOSQL_INJECTION_MONGO);
|
|
123
89
|
} else if (enable === false) {
|
|
124
|
-
|
|
125
|
-
enabledRules.delete(
|
|
90
|
+
globalPolicy.enabledRules.delete(ruleId);
|
|
91
|
+
if (ruleId === Rule.NOSQL_INJECTION) globalPolicy.enabledRules.delete(Rule.NOSQL_INJECTION_MONGO);
|
|
126
92
|
}
|
|
127
93
|
}
|
|
94
|
+
logger.info({ enabledRules: Array.from(globalPolicy.enabledRules) }, 'Assess policy enabled rules updated');
|
|
128
95
|
}
|
|
129
96
|
|
|
130
97
|
if (msg.exclusions) {
|
|
131
|
-
|
|
132
|
-
|
|
98
|
+
const rawDtmList = [
|
|
99
|
+
// todo: NODE-3281 input exclusions
|
|
100
|
+
...(msg?.exclusions?.input || []),
|
|
101
|
+
...(msg?.exclusions?.url || []),
|
|
102
|
+
].filter((exclusion) => exclusion?.modes?.includes?.('assess'));
|
|
103
|
+
|
|
104
|
+
// reset global exclusion state
|
|
105
|
+
for (const type of Object.values(ExclusionType)) {
|
|
106
|
+
globalPolicy.exclusionMap.get(type).length = 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!rawDtmList.length) return;
|
|
133
110
|
|
|
134
|
-
|
|
111
|
+
for (const dtm of rawDtmList) {
|
|
112
|
+
// normalize different dtm types
|
|
113
|
+
dtm.type = dtm.type || 'URL';
|
|
114
|
+
const { type } = dtm;
|
|
115
|
+
const key = ExclusionType[type];
|
|
116
|
+
// defensive code against unanticipated DTM values
|
|
117
|
+
if (key) {
|
|
118
|
+
const Ctor = dtm.type === ExclusionType.URL ? UrlExclusion : InputExclusion;
|
|
119
|
+
globalPolicy.exclusionMap.get(dtm.type).push(new Ctor(dtm));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
logger.info({
|
|
124
|
+
exclusions: Object.fromEntries(globalPolicy.exclusionMap)
|
|
125
|
+
}, 'Assess exclusions updated (%s total)', rawDtmList.length);
|
|
126
|
+
}
|
|
135
127
|
});
|
|
136
128
|
|
|
137
129
|
/**
|
|
138
|
-
*
|
|
139
|
-
* inconsistent behavior if policy is updated during request handling.
|
|
130
|
+
* Generates the policy for the current request. We return copy of the global policy
|
|
131
|
+
* to avoid inconsistent behavior if policy is updated during request handling. In
|
|
132
|
+
* addition, the request policy is altered to account for any URL or Input exclusions.
|
|
133
|
+
* @param {string} uriPath
|
|
140
134
|
*/
|
|
141
135
|
return core.assess.getPolicy = function getPolicy({ uriPath } = {}) {
|
|
142
|
-
const _enabledRules = new Set(enabledRules);
|
|
136
|
+
const _enabledRules = new Set(globalPolicy.enabledRules);
|
|
137
|
+
const exclusionState = {
|
|
138
|
+
// types that can be disabled broadly
|
|
139
|
+
[ExclusionType.BODY]: { track: true, excludedRules: new Set() },
|
|
140
|
+
[ExclusionType.QUERYSTRING]: { track: true, excludedRules: new Set() },
|
|
141
|
+
// other types we check by name. parameter applies to body and query params
|
|
142
|
+
[ExclusionType.COOKIE]: [],
|
|
143
|
+
[ExclusionType.HEADER]: [],
|
|
144
|
+
[ExclusionType.PARAMETER]: [],
|
|
145
|
+
};
|
|
143
146
|
|
|
144
|
-
// Evaluate
|
|
145
|
-
// If one matches and applies to all rules, we return `null` for the policy value,
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
for (const urlExclusion of
|
|
147
|
+
// Evaluate URL exclusions.
|
|
148
|
+
// If one matches and applies to all rules, we return `null` for the policy value, which
|
|
149
|
+
// will disable assess for the request (via getSourceContext()). If specific rules are
|
|
150
|
+
// disabled, we remove them from the request policy's set of enabled rules.
|
|
151
|
+
for (const urlExclusion of globalPolicy.exclusionMap.get(ExclusionType.URL)) {
|
|
149
152
|
if (urlExclusion.matchesUriPath(uriPath)) {
|
|
150
153
|
if (!urlExclusion.rules?.size) {
|
|
151
154
|
core.logger.debug({
|
|
152
155
|
name: urlExclusion.name
|
|
153
|
-
}, '
|
|
156
|
+
}, 'All Assess rules have been disabled by URL exclusion');
|
|
154
157
|
return null;
|
|
155
158
|
} else {
|
|
156
159
|
for (const ruleId of urlExclusion.rules) {
|
|
157
|
-
|
|
160
|
+
_enabledRules.delete(ruleId);
|
|
158
161
|
}
|
|
159
162
|
core.logger.debug({
|
|
160
163
|
name: urlExclusion.name,
|
|
161
164
|
rules: Array.from(urlExclusion.rules),
|
|
162
|
-
}, '
|
|
165
|
+
}, 'Assess rules disabled by URL exclusion');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Process input exclusions that apply broadly: BODY, QUERYSTRING
|
|
171
|
+
for (const type of BROAD_INPUT_EXCLUSION_TYPES) {
|
|
172
|
+
const _policy = exclusionState[type];
|
|
173
|
+
for (const exclusion of globalPolicy.exclusionMap.get(type)) {
|
|
174
|
+
if (exclusion.matchesUriPath(uriPath)) {
|
|
175
|
+
if (exclusion.rules.size) {
|
|
176
|
+
for (const ruleId of exclusion.rules) {
|
|
177
|
+
_policy.excludedRules.add(ruleId);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
_policy.track = false;
|
|
181
|
+
_policy.excludedRules.clear();
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Filter input exclusions that will be used to get named input
|
|
188
|
+
// policies: COOKIE, HEADER, PARAMETER
|
|
189
|
+
for (const type of NAMED_INPUT_EXCLUSION_TYPES) {
|
|
190
|
+
for (const exclusion of globalPolicy.exclusionMap.get(type)) {
|
|
191
|
+
if (exclusion.matchesUriPath(uriPath)) {
|
|
192
|
+
exclusionState[type].push(exclusion);
|
|
163
193
|
}
|
|
164
194
|
}
|
|
165
195
|
}
|
|
166
196
|
|
|
167
|
-
// creates copy of local policy for request store
|
|
168
197
|
return {
|
|
169
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Enabled rules filtered by any applicable URL exclusions
|
|
200
|
+
*/
|
|
201
|
+
enabledRules: _enabledRules,
|
|
202
|
+
/**
|
|
203
|
+
* Used by source handler to get policy information for specific named inputs.
|
|
204
|
+
* @param {InputType} inputType
|
|
205
|
+
* @param {string} [fieldName]
|
|
206
|
+
* @returns {InputPolicy}
|
|
207
|
+
*/
|
|
208
|
+
getInputPolicy(inputType, fieldName) {
|
|
209
|
+
let exclusionsByType;
|
|
210
|
+
const inputPolicy = { track: true, excludedRules: new Set() };
|
|
211
|
+
|
|
212
|
+
const isBody = BODY_TYPES.includes(inputType);
|
|
213
|
+
|
|
214
|
+
if (isBody || inputType === InputType.QUERYSTRING) {
|
|
215
|
+
// these can be disabled broadly
|
|
216
|
+
const _policy = exclusionState[isBody ? ExclusionType.BODY : ExclusionType.QUERYSTRING];
|
|
217
|
+
if (!_policy.track) {
|
|
218
|
+
return DISABLED_INPUT_POLICY;
|
|
219
|
+
}
|
|
220
|
+
for (const ruleId of _policy.excludedRules) {
|
|
221
|
+
inputPolicy.excludedRules.add(ruleId);
|
|
222
|
+
}
|
|
223
|
+
exclusionsByType = exclusionState[ExclusionType.PARAMETER];
|
|
224
|
+
} else if (inputType === InputType.URL_PARAMETER) {
|
|
225
|
+
exclusionsByType = exclusionState[ExclusionType.PARAMETER];
|
|
226
|
+
} else if (inputType === InputType.HEADER) {
|
|
227
|
+
exclusionsByType = exclusionState[ExclusionType.HEADER];
|
|
228
|
+
} else if ([
|
|
229
|
+
InputType.COOKIE_NAME,
|
|
230
|
+
InputType.COOKIE_VALUE
|
|
231
|
+
].includes(inputType)) {
|
|
232
|
+
exclusionsByType = exclusionState[ExclusionType.COOKIE];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!exclusionsByType) {
|
|
236
|
+
return inputPolicy;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// check input name
|
|
240
|
+
for (const exclusion of exclusionsByType) {
|
|
241
|
+
if (exclusion.matchesInputName(fieldName)) {
|
|
242
|
+
if (exclusion.rules.size) {
|
|
243
|
+
for (const ruleId of exclusion.rules) {
|
|
244
|
+
inputPolicy.excludedRules.add(ruleId);
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
return DISABLED_INPUT_POLICY;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return inputPolicy;
|
|
253
|
+
},
|
|
170
254
|
};
|
|
171
255
|
};
|
|
172
256
|
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @typedef InputPolicy
|
|
260
|
+
* @property {boolean} track
|
|
261
|
+
* @property {Set<Rule>} excludedRules
|
|
262
|
+
*/
|
|
263
|
+
|
|
264
|
+
class UrlExclusion {
|
|
265
|
+
constructor(dtm) {
|
|
266
|
+
this._urlRegex = null;
|
|
267
|
+
this._urls = new Set();
|
|
268
|
+
this.name = dtm.name;
|
|
269
|
+
this.type = ExclusionType[dtm.type];
|
|
270
|
+
this.rules = new Set(dtm.assess_rules);
|
|
271
|
+
|
|
272
|
+
if (dtm.urls.length) {
|
|
273
|
+
const regexSegments = [];
|
|
274
|
+
for (const url of dtm.urls) {
|
|
275
|
+
if (shouldBeRegExp(url)) {
|
|
276
|
+
regexSegments.push(url);
|
|
277
|
+
} else {
|
|
278
|
+
this._urls.add(url);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (regexSegments.length) {
|
|
282
|
+
this._urlRegex = new RegExp(`^${join(regexSegments, '|')}$`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Checks whether the current URI path matches any of the exclusion's URL values.
|
|
289
|
+
* Exclusions that don't match for the current request will not be enabled. The
|
|
290
|
+
* interpretation of the DTM is that if its urls list is empty, then that means
|
|
291
|
+
* it should match all requestss (can be the case for input exclusions).
|
|
292
|
+
* @param {string} uriPath uri to check
|
|
293
|
+
* @returns {boolean}
|
|
294
|
+
*/
|
|
295
|
+
matchesUriPath(uriPath) {
|
|
296
|
+
return (!this._urlRegex && !this._urls.size) ||
|
|
297
|
+
this._urls.has(uriPath) ||
|
|
298
|
+
!!this._urlRegex?.test?.(uriPath);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
class InputExclusion extends UrlExclusion {
|
|
303
|
+
constructor(dtm) {
|
|
304
|
+
super(dtm);
|
|
305
|
+
this._inputNameRegex = null;
|
|
306
|
+
this._inputName = null;
|
|
307
|
+
|
|
308
|
+
// dtm.name value is null for BODY and QUERYSTRING types
|
|
309
|
+
if (dtm.name) {
|
|
310
|
+
if (shouldBeRegExp(dtm.name)) {
|
|
311
|
+
this._inputNameRegex = new RegExp(`^${dtm.name}$`);
|
|
312
|
+
} else {
|
|
313
|
+
this._inputName = dtm.name;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Checks if the provided name matches the value from the exclusion dtm.
|
|
320
|
+
* @param {string} name field name being evaluated
|
|
321
|
+
* @returns {boolean}
|
|
322
|
+
*/
|
|
323
|
+
matchesInputName(name) {
|
|
324
|
+
// BODY and QUERYSTRING always match since they apply broadly
|
|
325
|
+
if (!this._inputName && !this._inputNameRegex) return true;
|
|
326
|
+
return this._inputNameRegex ? this._inputNameRegex.test(name) : this._inputName === name;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function shouldBeRegExp(str) {
|
|
331
|
+
return str.indexOf('*') > 0 ||
|
|
332
|
+
str.indexOf('.') > 0 ||
|
|
333
|
+
str.indexOf('+') > 0 ||
|
|
334
|
+
str.indexOf('?') > 0 ||
|
|
335
|
+
str.indexOf('\\') > 0;
|
|
336
|
+
}
|
|
@@ -53,7 +53,7 @@ module.exports = function(core) {
|
|
|
53
53
|
case InstrumentationType.RULE: {
|
|
54
54
|
const [ruleId] = rest;
|
|
55
55
|
if (!ruleId) break;
|
|
56
|
-
if (!ctx.policy
|
|
56
|
+
if (!ctx.policy?.enabledRules?.has?.(ruleId) || ruleScopes.isLocked(ruleId)) return null;
|
|
57
57
|
break;
|
|
58
58
|
}
|
|
59
59
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/assess",
|
|
3
|
-
"version": "1.27.
|
|
3
|
+
"version": "1.27.2",
|
|
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)",
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
"types": "lib/index.d.ts",
|
|
12
12
|
"engines": {
|
|
13
13
|
"npm": ">=6.13.7 <7 || >= 8.3.1",
|
|
14
|
-
"node": ">= 14.
|
|
14
|
+
"node": ">= 14.18.0"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "../scripts/test.sh"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@contrast/common": "1.20.
|
|
20
|
+
"@contrast/common": "1.20.1",
|
|
21
21
|
"@contrast/distringuish": "^4.4.0",
|
|
22
|
-
"@contrast/scopes": "1.4.
|
|
22
|
+
"@contrast/scopes": "1.4.1"
|
|
23
23
|
}
|
|
24
24
|
}
|