@contrast/assess 1.18.0 → 1.19.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/constants.js +26 -0
- package/lib/crypto-analysis/common.js +20 -0
- package/lib/crypto-analysis/index.js +44 -0
- package/lib/crypto-analysis/install/crypto.js +151 -0
- package/lib/crypto-analysis/install/math.js +99 -0
- package/lib/dataflow/propagation/install/JSON/parse.js +12 -11
- package/lib/dataflow/propagation/install/JSON/stringify.js +1 -1
- package/lib/dataflow/propagation/install/ejs/escape-xml.js +2 -2
- package/lib/dataflow/propagation/install/ejs/index.js +1 -0
- package/lib/dataflow/propagation/install/ejs/template.js +77 -0
- package/lib/dataflow/propagation/install/util-format.js +9 -3
- package/lib/dataflow/sinks/install/child-process.js +20 -14
- package/lib/dataflow/sinks/install/eval.js +16 -14
- package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +14 -8
- package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +12 -5
- package/lib/dataflow/sinks/install/fs.js +7 -7
- package/lib/dataflow/sinks/install/function.js +8 -12
- package/lib/dataflow/sinks/install/http/request.js +16 -8
- package/lib/dataflow/sinks/install/http/server-response.js +11 -2
- package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +15 -8
- package/lib/dataflow/sinks/install/libxmljs.js +15 -10
- package/lib/dataflow/sinks/install/marsdb.js +13 -8
- package/lib/dataflow/sinks/install/mongodb.js +25 -15
- package/lib/dataflow/sinks/install/mssql.js +20 -9
- package/lib/dataflow/sinks/install/mysql.js +15 -8
- package/lib/dataflow/sinks/install/node-serialize.js +15 -17
- package/lib/dataflow/sinks/install/postgres.js +17 -4
- package/lib/dataflow/sinks/install/sequelize.js +16 -9
- package/lib/dataflow/sinks/install/sqlite3.js +20 -7
- package/lib/dataflow/sinks/install/vm.js +19 -17
- package/lib/dataflow/sources/install/http.js +14 -42
- package/lib/dataflow/sources/install/koa/index.js +1 -0
- package/lib/dataflow/sources/install/koa/koa-multer.js +102 -0
- package/lib/dataflow/sources/install/multer1.js +25 -51
- package/lib/dataflow/sources/install/querystring.js +1 -4
- package/lib/event-factory.js +47 -0
- package/lib/get-policy.js +68 -0
- package/lib/get-source-context.js +62 -0
- package/lib/index.d.ts +50 -0
- package/lib/index.js +20 -19
- package/lib/make-source-context.js +74 -0
- package/lib/response-scanning/handlers/index.js +55 -28
- package/lib/response-scanning/install/http.js +13 -7
- package/lib/rule-scopes.js +48 -0
- package/lib/session-configuration/handlers.js +4 -3
- package/lib/session-configuration/install/express-session.js +8 -2
- package/package.json +2 -2
|
@@ -23,36 +23,20 @@ module.exports = (core) => {
|
|
|
23
23
|
depHooks,
|
|
24
24
|
patcher,
|
|
25
25
|
logger,
|
|
26
|
-
assess,
|
|
27
|
-
scopes
|
|
26
|
+
assess: { dataflow: { sources } },
|
|
27
|
+
scopes,
|
|
28
28
|
} = core;
|
|
29
29
|
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
context: 'req',
|
|
34
|
-
data: req,
|
|
35
|
-
keys: ['file'],
|
|
36
|
-
name: 'multer',
|
|
37
|
-
inputType: InputType.BODY,
|
|
38
|
-
sourceContext,
|
|
39
|
-
stacktraceOpts: {
|
|
40
|
-
constructorOpt,
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
} catch (err) {
|
|
44
|
-
logger.error({ err }, 'error handling multer Assess dataflow req.file source');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
30
|
+
function handler(req, constructorOpt) {
|
|
31
|
+
const sourceContext = scopes.sources.getStore()?.assess;
|
|
32
|
+
if (!sourceContext) return;
|
|
47
33
|
|
|
48
|
-
|
|
49
|
-
let i;
|
|
50
|
-
for (i = 0; i < req.files.length; i++) {
|
|
34
|
+
function handle(context, data, key) {
|
|
51
35
|
try {
|
|
52
|
-
|
|
53
|
-
context
|
|
54
|
-
data
|
|
55
|
-
keys: [
|
|
36
|
+
sources.handle({
|
|
37
|
+
context,
|
|
38
|
+
data,
|
|
39
|
+
keys: [key],
|
|
56
40
|
name: 'multer',
|
|
57
41
|
inputType: InputType.BODY,
|
|
58
42
|
sourceContext,
|
|
@@ -61,26 +45,22 @@ module.exports = (core) => {
|
|
|
61
45
|
},
|
|
62
46
|
});
|
|
63
47
|
} catch (err) {
|
|
64
|
-
logger.error({ err }, 'error handling multer Assess dataflow
|
|
48
|
+
logger.error({ err }, 'error handling multer Assess dataflow %s.%s source', context, key);
|
|
65
49
|
}
|
|
66
50
|
}
|
|
67
|
-
}
|
|
68
51
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
});
|
|
82
|
-
} catch (err) {
|
|
83
|
-
logger.error({ err }, 'error handling multer Assess dataflow req.body source');
|
|
52
|
+
if (req.file) {
|
|
53
|
+
handle('req', req, 'file');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (Array.isArray(req.files)) {
|
|
57
|
+
for (let i = 0; i < req.files.length; i++) {
|
|
58
|
+
handle('req.files', req.files[i], i);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (req.body && Object.keys(req.body).length) {
|
|
63
|
+
handle('req', req, 'body');
|
|
84
64
|
}
|
|
85
65
|
}
|
|
86
66
|
|
|
@@ -97,14 +77,8 @@ module.exports = (core) => {
|
|
|
97
77
|
patchType,
|
|
98
78
|
pre(data) {
|
|
99
79
|
const [req, , next, hooked] = data.args;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!sourceContext) return;
|
|
103
|
-
|
|
104
|
-
data.args[2] = wrap(function (...args) {
|
|
105
|
-
if (req.file) handleFile(sourceContext, hooked, req);
|
|
106
|
-
if (Array.isArray(req.files)) handleFiles(sourceContext, hooked, req);
|
|
107
|
-
if (req.body && Object.keys(req.body).length) handleBody(sourceContext, hooked, req.body);
|
|
80
|
+
data.args[2] = scopes.wrap(function (...args) {
|
|
81
|
+
handler(req, hooked);
|
|
108
82
|
next(...args);
|
|
109
83
|
});
|
|
110
84
|
},
|
|
@@ -32,10 +32,7 @@ module.exports = (core) => {
|
|
|
32
32
|
const sourceContext = core.scopes.sources.getStore()?.assess;
|
|
33
33
|
const inputType = InputType.QUERYSTRING;
|
|
34
34
|
|
|
35
|
-
if (!sourceContext)
|
|
36
|
-
logger.error({ name }, 'unable to handle source. Missing `sourceContext`');
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
35
|
+
if (!sourceContext) return;
|
|
39
36
|
|
|
40
37
|
if (sourceContext.parsedQuery) {
|
|
41
38
|
logger.trace({ name }, 'values already tracked');
|
package/lib/event-factory.js
CHANGED
|
@@ -270,5 +270,52 @@ module.exports = function(core) {
|
|
|
270
270
|
return event;
|
|
271
271
|
};
|
|
272
272
|
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* @param {{
|
|
276
|
+
* context: string,
|
|
277
|
+
* name: string,
|
|
278
|
+
* moduleName: string,
|
|
279
|
+
* methodName: string,
|
|
280
|
+
* object: { value: any, tracked: boolean },
|
|
281
|
+
* args: any[],
|
|
282
|
+
* result: { value: vany, tracked: boolean },
|
|
283
|
+
* source: string,
|
|
284
|
+
* stacktraceOpts: { constructorOpt?: Function},
|
|
285
|
+
* }} data
|
|
286
|
+
* @returns {any}
|
|
287
|
+
*/
|
|
288
|
+
eventFactory.createCryptoAnalysisEvent = function(data) {
|
|
289
|
+
const {
|
|
290
|
+
name = '',
|
|
291
|
+
source,
|
|
292
|
+
stacktraceOpts,
|
|
293
|
+
} = data;
|
|
294
|
+
|
|
295
|
+
if (!name) {
|
|
296
|
+
logger.debug({ data }, 'no sink event name');
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!source || !source.match(annotationRegExp)) {
|
|
301
|
+
logger.debug({ data }, 'malformed or missing sink event source field');
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let stack;
|
|
306
|
+
if (config.assess.stacktraces !== 'NONE') {
|
|
307
|
+
stack = createSnapshot(stacktraceOpts)();
|
|
308
|
+
} else {
|
|
309
|
+
stack = [];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
data.stack = stack;
|
|
313
|
+
data.time = Date.now();
|
|
314
|
+
|
|
315
|
+
eventFactory.createdEvents.add(data);
|
|
316
|
+
|
|
317
|
+
return data;
|
|
318
|
+
};
|
|
319
|
+
|
|
273
320
|
return eventFactory;
|
|
274
321
|
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2023 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
|
+
Rule,
|
|
20
|
+
ResponseScanningRule,
|
|
21
|
+
SessionConfigurationRule,
|
|
22
|
+
Event,
|
|
23
|
+
} = require('@contrast/common');
|
|
24
|
+
|
|
25
|
+
const rulesIds = Object.values({
|
|
26
|
+
...Rule,
|
|
27
|
+
...ResponseScanningRule,
|
|
28
|
+
...SessionConfigurationRule,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {{
|
|
33
|
+
* config: import('@contrast/config').Config,
|
|
34
|
+
* logger: import('@contrast/logger').Logger,
|
|
35
|
+
* messages: import('@contrast/common').Messages,
|
|
36
|
+
* }} core
|
|
37
|
+
* @returns {import('@contrast/common').Installable}
|
|
38
|
+
*/
|
|
39
|
+
module.exports = function assess(core) {
|
|
40
|
+
const { logger, messages } = core;
|
|
41
|
+
|
|
42
|
+
const enabledRules = new Set(rulesIds);
|
|
43
|
+
|
|
44
|
+
messages.on(Event.SERVER_SETTINGS_UPDATE, (msg) => {
|
|
45
|
+
if (!msg.assess) return;
|
|
46
|
+
|
|
47
|
+
for (const ruleId of rulesIds) {
|
|
48
|
+
const enable = msg.assess[ruleId]?.enable;
|
|
49
|
+
if (enable === true) {
|
|
50
|
+
enabledRules.add(ruleId);
|
|
51
|
+
if (ruleId === Rule.NOSQL_INJECTION) enabledRules.add(Rule.NOSQL_INJECTION_MONGO);
|
|
52
|
+
} else if (enable === false) {
|
|
53
|
+
if (ruleId === Rule.NOSQL_INJECTION) enabledRules.delete(Rule.NOSQL_INJECTION_MONGO);
|
|
54
|
+
enabledRules.delete(ruleId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
logger.info({
|
|
58
|
+
enabledRules: Array.from(enabledRules)
|
|
59
|
+
}, 'Assess policy updated');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return core.assess.getPolicy = function getPolicy() {
|
|
63
|
+
// creates copy of local policy for request store
|
|
64
|
+
return {
|
|
65
|
+
enabledRules: new Set(enabledRules),
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2023 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 { InstrumentationType } = require('./constants');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {{
|
|
22
|
+
* assess: import('@contrast/assess').Assess,
|
|
23
|
+
* config: import('@contrast/config').Config,
|
|
24
|
+
* scopes: import('@contrast/scopes').Scopes,
|
|
25
|
+
* }} core
|
|
26
|
+
*/
|
|
27
|
+
module.exports = function(core) {
|
|
28
|
+
const {
|
|
29
|
+
config,
|
|
30
|
+
scopes: { sources, instrumentation },
|
|
31
|
+
assess: { ruleScopes },
|
|
32
|
+
} = core;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {import('./constants.js').InstrumentationType} type
|
|
36
|
+
* @returns {import('@contrast/assess').SourceContext|null} the assess store
|
|
37
|
+
*/
|
|
38
|
+
return core.assess.getSourceContext = function getSourceContext(type, ...rest) {
|
|
39
|
+
const ctx = sources.getStore()?.assess;
|
|
40
|
+
|
|
41
|
+
if (!ctx || instrumentation.isLocked()) return null;
|
|
42
|
+
|
|
43
|
+
switch (type) {
|
|
44
|
+
case InstrumentationType.PROPAGATOR: {
|
|
45
|
+
if (ctx.propagationEventsCount > config.assess.max_propagation_events) return null;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case InstrumentationType.SOURCE: {
|
|
49
|
+
if (ctx.sourceEventsCount > config.assess.max_context_source_events) return null;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case InstrumentationType.RULE: {
|
|
53
|
+
const [ruleId] = rest;
|
|
54
|
+
if (!ruleId) break;
|
|
55
|
+
if (!ctx.policy.enabledRules.has(ruleId) || ruleScopes.isLocked(ruleId)) return null;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return ctx;
|
|
61
|
+
};
|
|
62
|
+
};
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2023 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
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
16
|
+
import { Rule } from '@contrast/common';
|
|
17
|
+
|
|
18
|
+
export enum InstrumentationType {
|
|
19
|
+
SOURCE = 'source',
|
|
20
|
+
PROPAGATOR = 'propagator',
|
|
21
|
+
RULE = 'rule',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SourceContext {
|
|
25
|
+
reqData: object,
|
|
26
|
+
responseData: {
|
|
27
|
+
contentType: string,
|
|
28
|
+
},
|
|
29
|
+
policy: {
|
|
30
|
+
enabledRules: Set<string>,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Policy {
|
|
35
|
+
enabledRules: Set<Rule>,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RuleScopes {
|
|
39
|
+
run(ruleId: Rule, cb: void): void;
|
|
40
|
+
isLocked(ruleId: Rule): boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Assess {
|
|
44
|
+
getPolicy(): Policy,
|
|
45
|
+
getSourceContext(instrType?: InstrumentationType, opts?: any): SourceContext,
|
|
46
|
+
makeSourceContext(req: IncomingMessage, res: ServerResponse): SourceContext,
|
|
47
|
+
ruleScopes: RuleScopes,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getSourceContext(instrType?: InstrumentationType, ops?: any): SourceContext;
|
package/lib/index.js
CHANGED
|
@@ -16,32 +16,33 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
|
-
const sessionConfiguration = require('./session-configuration');
|
|
20
|
-
const dataflow = require('./dataflow');
|
|
21
|
-
const responseScanning = require('./response-scanning');
|
|
22
|
-
const eventFactory = require('./event-factory');
|
|
23
19
|
|
|
24
20
|
module.exports = function assess(core) {
|
|
25
|
-
const assess = core.assess = {
|
|
21
|
+
const assess = core.assess = {
|
|
22
|
+
install() {
|
|
23
|
+
if (!core.config.getEffectiveValue('assess.enable')) {
|
|
24
|
+
core.logger.debug('assess is disabled, skipping installation');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
core.rewriter.install('assess');
|
|
29
|
+
callChildComponentMethodsSync(core.assess, 'install');
|
|
30
|
+
},
|
|
31
|
+
};
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
require('./rule-scopes')(core);
|
|
34
|
+
require('./get-policy')(core);
|
|
35
|
+
require('./make-source-context')(core);
|
|
36
|
+
require('./get-source-context')(core);
|
|
37
|
+
require('./event-factory')(core);
|
|
38
|
+
require('./crypto-analysis')(core);
|
|
39
|
+
require('./dataflow')(core);
|
|
40
|
+
require('./response-scanning')(core);
|
|
41
|
+
require('./session-configuration')(core);
|
|
31
42
|
|
|
32
43
|
// todo
|
|
33
44
|
// crypto rule implementations
|
|
34
45
|
// static rule implementations in coordination with (CLI) rewriter
|
|
35
46
|
|
|
36
|
-
assess.install = function() {
|
|
37
|
-
if (!core.config.getEffectiveValue('assess.enable')) {
|
|
38
|
-
core.logger.debug('assess is disabled, skipping installation');
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
core.rewriter.install('assess');
|
|
43
|
-
callChildComponentMethodsSync(core.assess, 'install');
|
|
44
|
-
};
|
|
45
|
-
|
|
46
47
|
return assess;
|
|
47
48
|
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2023 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 { toLowerCase } = require('@contrast/common');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {{
|
|
22
|
+
* assess: import('@contrast/assess').Assess,
|
|
23
|
+
* logger: import('@contrast/logger').Logger,
|
|
24
|
+
* }} core
|
|
25
|
+
*/
|
|
26
|
+
module.exports = function(core) {
|
|
27
|
+
const {
|
|
28
|
+
assess: { getPolicy },
|
|
29
|
+
logger,
|
|
30
|
+
} = core;
|
|
31
|
+
|
|
32
|
+
return core.assess.makeSourceContext = function (req, res) {
|
|
33
|
+
let contentType, queries, uriPath;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const ix = req.url.indexOf('?');
|
|
37
|
+
if (ix >= 0) {
|
|
38
|
+
uriPath = req.url.slice(0, ix);
|
|
39
|
+
queries = req.url.slice(ix + 1);
|
|
40
|
+
} else {
|
|
41
|
+
uriPath = req.url;
|
|
42
|
+
queries = '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// copy to avoid storing tracked values
|
|
46
|
+
const headers = { ...req.headers };
|
|
47
|
+
if (headers['content-type']) {
|
|
48
|
+
contentType = toLowerCase(headers['content-type']);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
propagationEventsCount: 0,
|
|
53
|
+
sourceEventsCount: 0,
|
|
54
|
+
policy: getPolicy(),
|
|
55
|
+
reqData: {
|
|
56
|
+
ip: req.socket.remoteAddress,
|
|
57
|
+
httpVersion: req.httpVersion,
|
|
58
|
+
method: req.method,
|
|
59
|
+
headers,
|
|
60
|
+
uriPath,
|
|
61
|
+
queries,
|
|
62
|
+
contentType,
|
|
63
|
+
},
|
|
64
|
+
responseData: {}
|
|
65
|
+
};
|
|
66
|
+
} catch (err) {
|
|
67
|
+
logger.error(
|
|
68
|
+
{ err },
|
|
69
|
+
'unable to construct assess store. assess will be disabled for request.'
|
|
70
|
+
);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
};
|
|
@@ -33,6 +33,19 @@ const {
|
|
|
33
33
|
checkCspSources
|
|
34
34
|
} = require('./utils');
|
|
35
35
|
|
|
36
|
+
const {
|
|
37
|
+
AUTOCOMPLETE_MISSING,
|
|
38
|
+
CACHE_CONTROLS_MISSING,
|
|
39
|
+
CLICKJACKING_CONTROL_MISSING,
|
|
40
|
+
CSP_HEADER_MISSING,
|
|
41
|
+
CSP_HEADER_INSECURE,
|
|
42
|
+
HSTS_HEADER_MISSING,
|
|
43
|
+
PARAMETER_POLLUTION,
|
|
44
|
+
XCONTENTTYPE_HEADER_MISSING,
|
|
45
|
+
X_POWERED_BY_HEADER,
|
|
46
|
+
XXSPROTECTION_HEADER_DISABLED,
|
|
47
|
+
} = ResponseScanningRule;
|
|
48
|
+
|
|
36
49
|
module.exports = function(core) {
|
|
37
50
|
const {
|
|
38
51
|
assess: {
|
|
@@ -44,7 +57,10 @@ module.exports = function(core) {
|
|
|
44
57
|
} = core;
|
|
45
58
|
|
|
46
59
|
responseScanning.handleAutoCompleteMissing = function(sourceContext, { responseHeaders, responseBody }) {
|
|
47
|
-
if (
|
|
60
|
+
if (
|
|
61
|
+
!isEnabled(AUTOCOMPLETE_MISSING, sourceContext) ||
|
|
62
|
+
!isHtmlContent(responseHeaders)
|
|
63
|
+
) {
|
|
48
64
|
return;
|
|
49
65
|
}
|
|
50
66
|
|
|
@@ -72,7 +88,10 @@ module.exports = function(core) {
|
|
|
72
88
|
const instructions = [];
|
|
73
89
|
|
|
74
90
|
// de-dupe; this will be re-emitted for parseableBody handlers anyway
|
|
75
|
-
if (
|
|
91
|
+
if (
|
|
92
|
+
!isEnabled(CACHE_CONTROLS_MISSING, sourceContext) ||
|
|
93
|
+
(isParseableResponse(responseHeaders) && !responseBody)
|
|
94
|
+
) {
|
|
76
95
|
return;
|
|
77
96
|
}
|
|
78
97
|
const cacheControlHeader = responseHeaders['cache-control'];
|
|
@@ -118,6 +137,8 @@ module.exports = function(core) {
|
|
|
118
137
|
};
|
|
119
138
|
|
|
120
139
|
responseScanning.handleClickJackingControlsMissing = function(sourceContext, { responseHeaders }) {
|
|
140
|
+
if (!isEnabled(CLICKJACKING_CONTROL_MISSING, sourceContext)) return;
|
|
141
|
+
|
|
121
142
|
// look for x-frame-options headers with deny or sameorigin
|
|
122
143
|
const xFrameHeaders = responseHeaders['x-frame-options'];
|
|
123
144
|
let hasFrameBusting = false;
|
|
@@ -135,6 +156,8 @@ module.exports = function(core) {
|
|
|
135
156
|
};
|
|
136
157
|
|
|
137
158
|
responseScanning.handleParameterPollution = function(sourceContext, { responseBody }) {
|
|
159
|
+
if (!isEnabled(PARAMETER_POLLUTION, sourceContext)) return;
|
|
160
|
+
|
|
138
161
|
// look for form tag with missing action attribute.
|
|
139
162
|
// ex: <form method="post">..
|
|
140
163
|
const elements = getElements('form', responseBody);
|
|
@@ -154,21 +177,23 @@ module.exports = function(core) {
|
|
|
154
177
|
};
|
|
155
178
|
|
|
156
179
|
/**
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
180
|
+
* Checks the response headers for the CSP. If found, and insecure, will return
|
|
181
|
+
* the evidence for reporting.
|
|
182
|
+
*
|
|
183
|
+
* @param {Object} responseHeaders - HTTP headers object.
|
|
184
|
+
* @returns {Object} - Evidence for insecure CSP header.
|
|
185
|
+
*/
|
|
163
186
|
responseScanning.handleCspHeader = function(sourceContext, { responseHeaders }) {
|
|
164
187
|
const cspHeaders = getCspHeaders(responseHeaders);
|
|
165
188
|
|
|
166
189
|
// Don't report if not set; this report belongs to 'csp-header-missing'
|
|
167
|
-
if (!cspHeaders) {
|
|
190
|
+
if (!cspHeaders && isEnabled(CSP_HEADER_MISSING, sourceContext)) {
|
|
168
191
|
reportFindings(sourceContext, { ruleId: ResponseScanningRule.CSP_HEADER_MISSING });
|
|
169
192
|
return;
|
|
170
193
|
}
|
|
171
194
|
|
|
195
|
+
if (!isEnabled(CSP_HEADER_INSECURE, sourceContext)) return;
|
|
196
|
+
|
|
172
197
|
const vulnerabilityMetadata = checkCspSources(cspHeaders);
|
|
173
198
|
|
|
174
199
|
if (vulnerabilityMetadata.insecure) {
|
|
@@ -189,6 +214,8 @@ module.exports = function(core) {
|
|
|
189
214
|
};
|
|
190
215
|
|
|
191
216
|
responseScanning.handleHstsHeaderMissing = function(sourceContext, { responseHeaders }) {
|
|
217
|
+
if (!isEnabled(HSTS_HEADER_MISSING, sourceContext)) return;
|
|
218
|
+
|
|
192
219
|
let header = responseHeaders['strict-transport-security'];
|
|
193
220
|
let maxAge;
|
|
194
221
|
|
|
@@ -219,6 +246,8 @@ module.exports = function(core) {
|
|
|
219
246
|
};
|
|
220
247
|
|
|
221
248
|
responseScanning.handleXContentTypeHeaderMissing = function(sourceContext, { responseHeaders }) {
|
|
249
|
+
if (!isEnabled(XCONTENTTYPE_HEADER_MISSING, sourceContext)) return;
|
|
250
|
+
|
|
222
251
|
const headerName = 'x-content-type-options';
|
|
223
252
|
let header = responseHeaders[headerName];
|
|
224
253
|
|
|
@@ -237,44 +266,42 @@ module.exports = function(core) {
|
|
|
237
266
|
});
|
|
238
267
|
};
|
|
239
268
|
|
|
240
|
-
// NODE-3135
|
|
241
269
|
responseScanning.handleXPoweredByHeader = function(sourceContext, { responseHeaders }) {
|
|
270
|
+
if (!isEnabled(X_POWERED_BY_HEADER, sourceContext)) return;
|
|
271
|
+
|
|
242
272
|
const headerName = 'x-powered-by';
|
|
243
273
|
let header = responseHeaders[headerName];
|
|
244
274
|
|
|
245
275
|
if (header) {
|
|
246
276
|
header = toLowerCase(header);
|
|
247
277
|
|
|
248
|
-
const instructions = [
|
|
249
|
-
{
|
|
250
|
-
type: 'Header',
|
|
251
|
-
name: headerName,
|
|
252
|
-
value: header
|
|
253
|
-
}
|
|
254
|
-
];
|
|
255
|
-
|
|
256
278
|
reportFindings(sourceContext, {
|
|
257
279
|
ruleId: ResponseScanningRule.X_POWERED_BY_HEADER,
|
|
258
280
|
vulnerabilityMetadata: {
|
|
259
|
-
|
|
281
|
+
platform: header,
|
|
260
282
|
}
|
|
261
283
|
});
|
|
262
284
|
}
|
|
263
285
|
};
|
|
264
286
|
|
|
265
287
|
responseScanning.handleXxsProtectionHeaderDisabled = function(sourceContext, { responseHeaders }) {
|
|
266
|
-
|
|
288
|
+
if (!isEnabled(XXSPROTECTION_HEADER_DISABLED, sourceContext)) return;
|
|
267
289
|
|
|
268
|
-
|
|
290
|
+
const header = responseHeaders['x-xss-protection'];
|
|
269
291
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
292
|
+
if (header && !header.startsWith('1')) {
|
|
293
|
+
reportFindings(sourceContext, {
|
|
294
|
+
ruleId: ResponseScanningRule.XXSPROTECTION_HEADER_DISABLED,
|
|
295
|
+
vulnerabilityMetadata: {
|
|
296
|
+
data: header,
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
276
300
|
};
|
|
277
301
|
|
|
302
|
+
function isEnabled(ruleId, sourceContext) {
|
|
303
|
+
return !!sourceContext?.policy?.enabledRules?.has?.(ruleId);
|
|
304
|
+
}
|
|
305
|
+
|
|
278
306
|
return responseScanning;
|
|
279
307
|
};
|
|
280
|
-
|