@contrast/assess 1.63.0 → 1.65.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/{session-configuration → configuration-analysis}/common.js +1 -1
- package/lib/{session-configuration → configuration-analysis}/handlers.js +24 -11
- package/lib/{session-configuration → configuration-analysis}/index.js +6 -4
- package/lib/configuration-analysis/install/apollo-server.js +92 -0
- package/lib/{session-configuration → configuration-analysis}/install/express-session.js +2 -2
- package/lib/{session-configuration → configuration-analysis}/install/fastify-cookie.js +2 -2
- package/lib/configuration-analysis/install/graphql-yoga.js +90 -0
- package/lib/{session-configuration → configuration-analysis}/install/hapi.js +2 -2
- package/lib/{session-configuration → configuration-analysis}/install/koa.js +3 -3
- package/lib/dataflow/propagation/install/string/substring.js +1 -1
- package/lib/dataflow/sources/handler.js +30 -26
- package/lib/dataflow/sources/index.js +2 -0
- package/lib/dataflow/sources/install/fastify-websocket.js +63 -0
- package/lib/dataflow/sources/install/http.js +42 -38
- package/lib/dataflow/sources/install/koa/index.js +1 -1
- package/lib/dataflow/sources/install/koa/koa-bodyparsers.js +76 -48
- package/lib/dataflow/sources/install/koa/koa-multer.js +1 -1
- package/lib/dataflow/sources/install/koa/koa-routers.js +2 -2
- package/lib/dataflow/sources/install/koa/{koa2.js → koa.js} +3 -3
- package/lib/dataflow/sources/install/socket.io.js +80 -0
- package/lib/get-source-context.js +10 -21
- package/lib/index.d.ts +4 -3
- package/lib/index.js +2 -2
- package/lib/make-source-context.js +5 -10
- package/lib/policy.js +400 -0
- package/lib/response-scanning/handlers/index.js +10 -14
- package/package.json +12 -12
- package/lib/get-policy.js +0 -336
package/lib/get-policy.js
DELETED
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright: 2025 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
|
-
Event,
|
|
20
|
-
ExclusionType,
|
|
21
|
-
InputType,
|
|
22
|
-
Rule,
|
|
23
|
-
ResponseScanningRule,
|
|
24
|
-
SessionConfigurationRule,
|
|
25
|
-
primordials: { ArrayPrototypeJoin, RegExpPrototypeTest }
|
|
26
|
-
} = require('@contrast/common');
|
|
27
|
-
|
|
28
|
-
const ASSESS_RULES = Object.values({
|
|
29
|
-
...Rule,
|
|
30
|
-
...ResponseScanningRule,
|
|
31
|
-
...SessionConfigurationRule,
|
|
32
|
-
});
|
|
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 };
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* @param {{
|
|
55
|
-
* config: import('@contrast/config').Config,
|
|
56
|
-
* logger: import('@contrast/logger').Logger,
|
|
57
|
-
* messages: import('@contrast/common').Messages,
|
|
58
|
-
* }} core
|
|
59
|
-
* @returns {import('@contrast/common').Installable}
|
|
60
|
-
*/
|
|
61
|
-
module.exports = function assess(core) {
|
|
62
|
-
const { config, logger, messages } = core;
|
|
63
|
-
|
|
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
|
-
]),
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Subscribe to settings updates and modify global policy accordingly.
|
|
79
|
-
*/
|
|
80
|
-
messages.on(Event.SERVER_SETTINGS_UPDATE, (msg) => {
|
|
81
|
-
if (!config.getEffectiveValue('assess.enable')) return;
|
|
82
|
-
|
|
83
|
-
if (msg.assess) {
|
|
84
|
-
for (const ruleId of ASSESS_RULES) {
|
|
85
|
-
const enable = msg.assess[ruleId]?.enable;
|
|
86
|
-
if (enable === true) {
|
|
87
|
-
globalPolicy.enabledRules.add(ruleId);
|
|
88
|
-
if (ruleId === Rule.NOSQL_INJECTION) globalPolicy.enabledRules.add(Rule.NOSQL_INJECTION_MONGO);
|
|
89
|
-
} else if (enable === false) {
|
|
90
|
-
globalPolicy.enabledRules.delete(ruleId);
|
|
91
|
-
if (ruleId === Rule.NOSQL_INJECTION) globalPolicy.enabledRules.delete(Rule.NOSQL_INJECTION_MONGO);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
logger.info({ enabledRules: Array.from(globalPolicy.enabledRules) }, 'Assess policy enabled rules updated');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (msg.exclusions) {
|
|
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;
|
|
110
|
-
|
|
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
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
/**
|
|
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
|
|
134
|
-
*/
|
|
135
|
-
return core.assess.getPolicy = function getPolicy({ uriPath } = {}) {
|
|
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
|
-
};
|
|
146
|
-
|
|
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)) {
|
|
152
|
-
if (urlExclusion.matchesUriPath(uriPath)) {
|
|
153
|
-
if (!urlExclusion.rules?.size) {
|
|
154
|
-
core.logger.debug({
|
|
155
|
-
name: urlExclusion.name
|
|
156
|
-
}, 'All Assess rules have been disabled by URL exclusion');
|
|
157
|
-
return null;
|
|
158
|
-
} else {
|
|
159
|
-
for (const ruleId of urlExclusion.rules) {
|
|
160
|
-
_enabledRules.delete(ruleId);
|
|
161
|
-
}
|
|
162
|
-
core.logger.debug({
|
|
163
|
-
name: urlExclusion.name,
|
|
164
|
-
rules: Array.from(urlExclusion.rules),
|
|
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);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return {
|
|
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
|
-
},
|
|
254
|
-
};
|
|
255
|
-
};
|
|
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(`^${ArrayPrototypeJoin.call(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 ? RegExpPrototypeTest.call(this._inputNameRegex, 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
|
-
}
|