@contrast/assess 1.64.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 +23 -10
- 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 +9 -2
- 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/index.d.ts +4 -3
- package/lib/index.js +1 -1
- package/lib/policy.js +2 -2
- package/package.json +12 -12
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
|
|
18
18
|
const {
|
|
19
19
|
Event,
|
|
20
|
-
|
|
20
|
+
ConfigurationRule,
|
|
21
21
|
isString,
|
|
22
22
|
} = require('@contrast/common');
|
|
23
23
|
|
|
24
|
-
const { HTTPONLY, SECURE_FLAG_MISSING } =
|
|
24
|
+
const { HTTPONLY, SECURE_FLAG_MISSING, GRAPHQL_INTROSPECTION } = ConfigurationRule;
|
|
25
25
|
|
|
26
26
|
module.exports = function (core) {
|
|
27
27
|
const {
|
|
28
|
-
assess: {
|
|
28
|
+
assess: { configurationAnalysis },
|
|
29
29
|
messages,
|
|
30
30
|
} = core;
|
|
31
31
|
|
|
@@ -40,7 +40,7 @@ module.exports = function (core) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* @param {
|
|
43
|
+
* @param {ConfigurationRule} ruleId
|
|
44
44
|
* @param {import('@contrast/assess').SourceContext} sourceContext
|
|
45
45
|
* @returns {import('@contrast/assess').SessionRuleState}
|
|
46
46
|
*/
|
|
@@ -76,7 +76,7 @@ module.exports = function (core) {
|
|
|
76
76
|
if (!isVulnerable(ruleId, value)) continue;
|
|
77
77
|
|
|
78
78
|
else {
|
|
79
|
-
|
|
79
|
+
configurationAnalysis.reportFindings({
|
|
80
80
|
ruleId,
|
|
81
81
|
sinkEvent: sessionEvent,
|
|
82
82
|
properties: {
|
|
@@ -89,17 +89,30 @@ module.exports = function (core) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
configurationAnalysis.handleHttpOnly = function(sourceContext, cookie, sessionEvent) {
|
|
93
93
|
handle(HTTPONLY, sourceContext, cookie, sessionEvent);
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
configurationAnalysis.handleSecure = function (sourceContext, cookie, sessionEvent) {
|
|
97
97
|
handle(SECURE_FLAG_MISSING, sourceContext, cookie, sessionEvent);
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
configurationAnalysis.handleGraphqlIntrospection = function (sourceContext, sessionEvent, value) {
|
|
101
|
+
const ruleId = GRAPHQL_INTROSPECTION;
|
|
102
|
+
const state = ensureState(ruleId, sourceContext);
|
|
103
|
+
if (sourceContext?.policy?.disabledRules?.has?.(ruleId) || state.reported) return;
|
|
104
|
+
|
|
105
|
+
configurationAnalysis.reportFindings({
|
|
106
|
+
ruleId,
|
|
107
|
+
sinkEvent: sessionEvent,
|
|
108
|
+
evidence: value
|
|
109
|
+
});
|
|
110
|
+
state.reported = true;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
configurationAnalysis.reportFindings = function (finding) {
|
|
114
|
+
messages.emit(Event.ASSESS_CONFIGURATION_FINDING, finding);
|
|
102
115
|
};
|
|
103
116
|
|
|
104
|
-
return
|
|
117
|
+
return configurationAnalysis;
|
|
105
118
|
};
|
|
@@ -18,17 +18,19 @@
|
|
|
18
18
|
const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
19
|
|
|
20
20
|
module.exports = function(core) {
|
|
21
|
-
const
|
|
21
|
+
const configurationAnalysis = core.assess.configurationAnalysis = {};
|
|
22
22
|
|
|
23
23
|
require('./handlers')(core);
|
|
24
|
+
require('./install/apollo-server')(core);
|
|
25
|
+
require('./install/graphql-yoga')(core);
|
|
24
26
|
require('./install/express-session')(core);
|
|
25
27
|
require('./install/fastify-cookie')(core);
|
|
26
28
|
require('./install/hapi')(core);
|
|
27
29
|
require('./install/koa')(core);
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
callChildComponentMethodsSync(
|
|
31
|
+
configurationAnalysis.install = function() {
|
|
32
|
+
callChildComponentMethodsSync(configurationAnalysis, 'install');
|
|
31
33
|
};
|
|
32
34
|
|
|
33
|
-
return
|
|
35
|
+
return configurationAnalysis;
|
|
34
36
|
};
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { patchType } = require('../common');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{
|
|
21
|
+
* assess: import('@contrast/assess').Assess,
|
|
22
|
+
* scopes: import('@contrast/scopes').Scopes,
|
|
23
|
+
* }} core
|
|
24
|
+
*/
|
|
25
|
+
module.exports = function (core) {
|
|
26
|
+
const {
|
|
27
|
+
assess: {
|
|
28
|
+
inspect, // TODO NODE-3455: remove
|
|
29
|
+
getSourceContext,
|
|
30
|
+
eventFactory: { createSessionEvent },
|
|
31
|
+
configurationAnalysis: {
|
|
32
|
+
handleGraphqlIntrospection
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
depHooks,
|
|
36
|
+
patcher,
|
|
37
|
+
} = core;
|
|
38
|
+
|
|
39
|
+
const apolloServer = core.assess.configurationAnalysis.apolloServer = {};
|
|
40
|
+
|
|
41
|
+
apolloServer.install = function () {
|
|
42
|
+
return depHooks.resolve({ name: '@apollo/server', version: '>=4', file: 'dist/cjs' }, (xport) => {
|
|
43
|
+
if (!xport.ApolloServer) return;
|
|
44
|
+
patcher.patch(xport, 'ApolloServer', {
|
|
45
|
+
name: '@apollo/server.ApolloServer',
|
|
46
|
+
patchType,
|
|
47
|
+
post(data) {
|
|
48
|
+
if (!data.args[0]?.introspection) return;
|
|
49
|
+
|
|
50
|
+
const options = { introspection: true };
|
|
51
|
+
const optionsString = inspect(options);
|
|
52
|
+
const sessionEvent = createSessionEvent({
|
|
53
|
+
args: [{
|
|
54
|
+
tracked: false,
|
|
55
|
+
value: optionsString,
|
|
56
|
+
}],
|
|
57
|
+
context: optionsString,
|
|
58
|
+
name: '@apollo/server',
|
|
59
|
+
moduleName: 'ApolloServer',
|
|
60
|
+
methodName: '',
|
|
61
|
+
object: {
|
|
62
|
+
tracked: false,
|
|
63
|
+
value: 'ApolloServer',
|
|
64
|
+
},
|
|
65
|
+
result: {
|
|
66
|
+
tracked: false,
|
|
67
|
+
},
|
|
68
|
+
source: 'P0',
|
|
69
|
+
stacktraceOpts: {
|
|
70
|
+
constructorOpt: data.hooked,
|
|
71
|
+
},
|
|
72
|
+
framework: 'graphql',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
patcher.patch(data.result, 'executeHTTPGraphQLRequest', {
|
|
76
|
+
name: 'ApolloServer.executeHTTPGraphQLRequest',
|
|
77
|
+
patchType,
|
|
78
|
+
post(data) {
|
|
79
|
+
const sourceContext = getSourceContext();
|
|
80
|
+
if (!sourceContext) return;
|
|
81
|
+
|
|
82
|
+
handleGraphqlIntrospection(sourceContext, sessionEvent, optionsString);
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return apolloServer;
|
|
92
|
+
};
|
|
@@ -29,7 +29,7 @@ module.exports = function (core) {
|
|
|
29
29
|
inspect, // TODO NODE-3455: remove
|
|
30
30
|
getSourceContext,
|
|
31
31
|
eventFactory: { createSessionEvent },
|
|
32
|
-
|
|
32
|
+
configurationAnalysis: {
|
|
33
33
|
handleHttpOnly,
|
|
34
34
|
handleSecure,
|
|
35
35
|
},
|
|
@@ -38,7 +38,7 @@ module.exports = function (core) {
|
|
|
38
38
|
patcher,
|
|
39
39
|
} = core;
|
|
40
40
|
|
|
41
|
-
const expressSession = core.assess.
|
|
41
|
+
const expressSession = core.assess.configurationAnalysis.expressSession = {};
|
|
42
42
|
|
|
43
43
|
expressSession.install = function () {
|
|
44
44
|
return depHooks.resolve({ name: 'express-session', version: '<2' }, (session) => {
|
|
@@ -29,7 +29,7 @@ module.exports = function (core) {
|
|
|
29
29
|
inspect, // TODO NODE-3455: remove
|
|
30
30
|
getSourceContext,
|
|
31
31
|
eventFactory: { createSessionEvent },
|
|
32
|
-
|
|
32
|
+
configurationAnalysis: {
|
|
33
33
|
handleHttpOnly,
|
|
34
34
|
handleSecure,
|
|
35
35
|
},
|
|
@@ -38,7 +38,7 @@ module.exports = function (core) {
|
|
|
38
38
|
patcher,
|
|
39
39
|
} = core;
|
|
40
40
|
|
|
41
|
-
return core.assess.
|
|
41
|
+
return core.assess.configurationAnalysis.fastifyCookie = {
|
|
42
42
|
install () {
|
|
43
43
|
depHooks.resolve({ name: '@fastify/cookie', version: '<12' }, (_export) => {
|
|
44
44
|
const patched = patcher.patch(_export, {
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { patchType } = require('../common');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{
|
|
21
|
+
* assess: import('@contrast/assess').Assess,
|
|
22
|
+
* scopes: import('@contrast/scopes').Scopes,
|
|
23
|
+
* }} core
|
|
24
|
+
*/
|
|
25
|
+
module.exports = function (core) {
|
|
26
|
+
const {
|
|
27
|
+
assess: {
|
|
28
|
+
inspect, // TODO NODE-3455: remove
|
|
29
|
+
getSourceContext,
|
|
30
|
+
eventFactory: { createSessionEvent },
|
|
31
|
+
configurationAnalysis: {
|
|
32
|
+
handleGraphqlIntrospection
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
depHooks,
|
|
36
|
+
patcher,
|
|
37
|
+
} = core;
|
|
38
|
+
|
|
39
|
+
const graphqlYoga = core.assess.configurationAnalysis.graphqlYoga = {};
|
|
40
|
+
|
|
41
|
+
graphqlYoga.install = function () {
|
|
42
|
+
return depHooks.resolve({ name: '@graphql-yoga/plugin-disable-introspection', version: '*', file: 'cjs' }, (xport) => patcher.patch(xport, 'useDisableIntrospection', {
|
|
43
|
+
name: '@graphql-yoga/plugin-disable-introspection.useDisableIntrospection',
|
|
44
|
+
patchType,
|
|
45
|
+
post(data) {
|
|
46
|
+
const options = data.args[0];
|
|
47
|
+
const optionsString = inspect(options);
|
|
48
|
+
patcher.patch(data.result, 'onValidate', {
|
|
49
|
+
name: 'onValidate',
|
|
50
|
+
patchType,
|
|
51
|
+
pre(data) {
|
|
52
|
+
patcher.patch(data.args[0], 'addValidationRule', {
|
|
53
|
+
name: 'addValidationRule',
|
|
54
|
+
patchType,
|
|
55
|
+
post(data) {
|
|
56
|
+
const sourceContext = getSourceContext();
|
|
57
|
+
if (!sourceContext) return;
|
|
58
|
+
const sessionEvent = createSessionEvent({
|
|
59
|
+
args: [{
|
|
60
|
+
tracked: false,
|
|
61
|
+
value: optionsString,
|
|
62
|
+
}],
|
|
63
|
+
context: optionsString,
|
|
64
|
+
name: '@graphql-yoga',
|
|
65
|
+
moduleName: 'plugin-disable-introspection',
|
|
66
|
+
methodName: 'addValidationRule',
|
|
67
|
+
object: {
|
|
68
|
+
tracked: false,
|
|
69
|
+
value: 'plugin-disable-introspection',
|
|
70
|
+
},
|
|
71
|
+
result: {
|
|
72
|
+
tracked: false,
|
|
73
|
+
},
|
|
74
|
+
source: 'P0',
|
|
75
|
+
stacktraceOpts: {
|
|
76
|
+
constructorOpt: data.hooked,
|
|
77
|
+
},
|
|
78
|
+
framework: 'graphql',
|
|
79
|
+
});
|
|
80
|
+
handleGraphqlIntrospection(sourceContext, sessionEvent, optionsString);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return graphqlYoga;
|
|
90
|
+
};
|
|
@@ -21,7 +21,7 @@ module.exports = function (core) {
|
|
|
21
21
|
assess: {
|
|
22
22
|
inspect, // TODO NODE-3455: remove
|
|
23
23
|
eventFactory: { createSessionEvent },
|
|
24
|
-
|
|
24
|
+
configurationAnalysis: {
|
|
25
25
|
handleHttpOnly,
|
|
26
26
|
handleSecure,
|
|
27
27
|
},
|
|
@@ -31,7 +31,7 @@ module.exports = function (core) {
|
|
|
31
31
|
scopes: { sources },
|
|
32
32
|
} = core;
|
|
33
33
|
|
|
34
|
-
const hapiSession = core.assess.
|
|
34
|
+
const hapiSession = core.assess.configurationAnalysis.hapiSession = {};
|
|
35
35
|
|
|
36
36
|
hapiSession.install = function () {
|
|
37
37
|
return depHooks.resolve({ name: '@hapi/hapi', version: '>=18 <22' }, (hapi) => {
|
|
@@ -28,7 +28,7 @@ module.exports = function (core) {
|
|
|
28
28
|
inspect, // TODO NODE-3455: remove
|
|
29
29
|
getSourceContext,
|
|
30
30
|
eventFactory: { createSessionEvent },
|
|
31
|
-
|
|
31
|
+
configurationAnalysis: {
|
|
32
32
|
handleHttpOnly,
|
|
33
33
|
handleSecure,
|
|
34
34
|
},
|
|
@@ -37,9 +37,9 @@ module.exports = function (core) {
|
|
|
37
37
|
patcher,
|
|
38
38
|
} = core;
|
|
39
39
|
|
|
40
|
-
return core.assess.
|
|
40
|
+
return core.assess.configurationAnalysis.koa = {
|
|
41
41
|
install () {
|
|
42
|
-
depHooks.resolve({ name: 'koa', version: '>=2.3.0 <
|
|
42
|
+
depHooks.resolve({ name: 'koa', version: '>=2.3.0 <4' }, (Koa) => {
|
|
43
43
|
patcher.patch(Koa.prototype, 'use', {
|
|
44
44
|
name: 'Koa.Application',
|
|
45
45
|
patchType,
|
|
@@ -89,7 +89,7 @@ module.exports = function(core) {
|
|
|
89
89
|
const event = createPropagationEvent({
|
|
90
90
|
name,
|
|
91
91
|
moduleName: 'String',
|
|
92
|
-
methodName:
|
|
92
|
+
methodName: `prototype.${method}`,
|
|
93
93
|
get context() {
|
|
94
94
|
return `'${objInfo.value}'.substring(${ArrayPrototypeJoin.call(args.map(a => a.value))})`;
|
|
95
95
|
},
|
|
@@ -76,6 +76,7 @@ module.exports = Core.makeComponent({
|
|
|
76
76
|
stacktraceOpts,
|
|
77
77
|
data,
|
|
78
78
|
sourceContext,
|
|
79
|
+
onEvent,
|
|
79
80
|
}) {
|
|
80
81
|
if (!data) return;
|
|
81
82
|
|
|
@@ -105,7 +106,7 @@ module.exports = Core.makeComponent({
|
|
|
105
106
|
}
|
|
106
107
|
// create the stacktrace once per call to .handle()
|
|
107
108
|
stack || (stack = sources.createStacktrace(stacktraceOpts));
|
|
108
|
-
|
|
109
|
+
const eventData = {
|
|
109
110
|
context: `${context}.${pathName}`,
|
|
110
111
|
name,
|
|
111
112
|
fieldName,
|
|
@@ -114,7 +115,12 @@ module.exports = Core.makeComponent({
|
|
|
114
115
|
inputType,
|
|
115
116
|
tags: sources.createTags({ inputType, fieldName, value, tagNames }),
|
|
116
117
|
result: { tracked: true, value },
|
|
117
|
-
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const event = eventFactory.createSourceEvent(eventData);;
|
|
121
|
+
if (event && onEvent) onEvent(event, fieldName, pathName);
|
|
122
|
+
|
|
123
|
+
return event;
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
if (Buffer.isBuffer(data) && !tracker.getData(data)) {
|
|
@@ -129,6 +135,7 @@ module.exports = Core.makeComponent({
|
|
|
129
135
|
|
|
130
136
|
const event = createEvent({ pathName: 'body', value: data, fieldName: '', excludedRules });
|
|
131
137
|
if (event) {
|
|
138
|
+
if (onEvent) onEvent(event);
|
|
132
139
|
tracker.track(data, event);
|
|
133
140
|
}
|
|
134
141
|
|
|
@@ -29,12 +29,14 @@ module.exports = function (core) {
|
|
|
29
29
|
require('./install/body-parser')(core);
|
|
30
30
|
require('./install/busboy')(core);
|
|
31
31
|
require('./install/cookie-parser1')(core);
|
|
32
|
+
core.initComponentSync(require('./install/fastify-websocket'));
|
|
32
33
|
require('./install/formidable1')(core);
|
|
33
34
|
require('./install/graphql-http')(core);
|
|
34
35
|
require('./install/http')(core);
|
|
35
36
|
require('./install/qs6')(core);
|
|
36
37
|
require('./install/querystring')(core);
|
|
37
38
|
require('./install/multer1')(core);
|
|
39
|
+
core.initComponentSync(require('./install/socket.io'));
|
|
38
40
|
|
|
39
41
|
sources.install = function install() {
|
|
40
42
|
callChildComponentMethodsSync(sources, 'install');
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { InputType, set } = require('@contrast/common');
|
|
4
|
+
const Core = require('@contrast/core/lib/ioc/core');
|
|
5
|
+
const { patchType } = require('../common');
|
|
6
|
+
|
|
7
|
+
const COMPONENT_NAME = 'assess.dataflow.sources.fastifyWebsocketInstrumentation';
|
|
8
|
+
|
|
9
|
+
module.exports = Core.makeComponent({
|
|
10
|
+
name: COMPONENT_NAME,
|
|
11
|
+
factory: (core) => new FastifyWebsocketAssessSource(core),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
class FastifyWebsocketAssessSource {
|
|
15
|
+
constructor(core) {
|
|
16
|
+
Object.defineProperty(this, 'core', { value: core });
|
|
17
|
+
set(core, COMPONENT_NAME, this);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Deploys @fastify/websocket instrumentation.
|
|
22
|
+
*/
|
|
23
|
+
install() {
|
|
24
|
+
const {
|
|
25
|
+
depHooks,
|
|
26
|
+
patcher,
|
|
27
|
+
assess,
|
|
28
|
+
} = this.core;
|
|
29
|
+
|
|
30
|
+
depHooks.resolve({ name: '@fastify/websocket', version: '*' }, (fws) => {
|
|
31
|
+
// patch exported function
|
|
32
|
+
return patcher.patch(fws, {
|
|
33
|
+
name: '@fastify/websocket',
|
|
34
|
+
patchType,
|
|
35
|
+
post(data) {
|
|
36
|
+
// the plugin decorates fastify with the ws.WebSocketServer instance.
|
|
37
|
+
// we use the connection event to get reference to connecting
|
|
38
|
+
// WebSockets, and track when they emit message buffers.
|
|
39
|
+
data.args[0].websocketServer?.on?.('connection', (socket) => {
|
|
40
|
+
socket.on('message', function handler(data) {
|
|
41
|
+
const sourceContext = assess.getSourceContext();
|
|
42
|
+
// this should be present since sources run 'upgrade' requests in request scope
|
|
43
|
+
if (!sourceContext) return;
|
|
44
|
+
|
|
45
|
+
// this will track the emitted buffer
|
|
46
|
+
assess.dataflow.sources.handle({
|
|
47
|
+
data,
|
|
48
|
+
name: 'fastify-websocket',
|
|
49
|
+
inputType: InputType.WEBSOCKET,
|
|
50
|
+
stacktraceOpts: { constructorOpt: handler },
|
|
51
|
+
sourceContext,
|
|
52
|
+
onEvent(event) {
|
|
53
|
+
event.context = 'WebSocket.on("message", ...args)';
|
|
54
|
+
event.args = [{ value: 'args.0', tracked: true }];
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
@@ -36,63 +36,68 @@ module.exports = function (core) {
|
|
|
36
36
|
const logger = core.logger.child({ name: 'contrast:assess' });
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* The around hook for `emit` that
|
|
40
|
-
*
|
|
39
|
+
* The around hook for `emit` that handles tracking URL and header values.
|
|
40
|
+
* We track those when the event is 'request' or 'upgrade'. Also, for when
|
|
41
|
+
* event is 'request', we will also patch some ServerResponse methods. We
|
|
42
|
+
* currentl don't patch the raw socket for tracking when event is 'upgrade',
|
|
43
|
+
* sources instrumentation for websocket events happens per framework.
|
|
41
44
|
*/
|
|
42
45
|
function around(next, data) {
|
|
43
46
|
const [type] = data.args;
|
|
44
47
|
|
|
45
|
-
if (type !== 'request') return next();
|
|
48
|
+
if (type !== 'request' && type !== 'upgrade') return next();
|
|
46
49
|
|
|
47
50
|
try {
|
|
48
|
-
const [, req,
|
|
51
|
+
const [, req, resOrSocket] = data.args;
|
|
49
52
|
const sourceContext = getSourceContext();
|
|
50
53
|
|
|
51
54
|
if (!sourceContext?.policy) {
|
|
52
55
|
return next();
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
if (type == 'request') {
|
|
59
|
+
patcher.patch(resOrSocket, 'writeHead', {
|
|
60
|
+
name: 'write-head',
|
|
61
|
+
patchType,
|
|
62
|
+
pre(data) {
|
|
63
|
+
const obj = data.args[data.args.length - 1];
|
|
64
|
+
if (!obj) return;
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
if (Array.isArray(obj)) {
|
|
67
|
+
for (let i = 0; i < obj.length; i += 2) {
|
|
68
|
+
const key = obj[i];
|
|
69
|
+
const value = obj[i + 1];
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
if (StringPrototypeToLowerCase.call(key) === 'content-type') {
|
|
72
|
+
sourceContext.responseData.contentType = value;
|
|
73
|
+
}
|
|
69
74
|
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
} else if (typeof obj === 'object') {
|
|
76
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
77
|
+
if (StringPrototypeToLowerCase.call(key) === 'content-type') {
|
|
78
|
+
sourceContext.responseData.contentType = value;
|
|
79
|
+
}
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
|
-
}
|
|
79
|
-
});
|
|
83
|
+
});
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
if (!patcher.hooks.get(resOrSocket?.setHeader)?.funcKeys.has(`${patchType}:set-header`)) {
|
|
86
|
+
patcher.patch(resOrSocket, 'setHeader', {
|
|
87
|
+
name: 'set-header',
|
|
88
|
+
patchType,
|
|
89
|
+
pre(data) {
|
|
90
|
+
const [name = '', value] = data.args;
|
|
91
|
+
if (
|
|
92
|
+
value &&
|
|
93
|
+
StringPrototypeToLowerCase.call(name) === 'content-type' &&
|
|
94
|
+
getSourceContext()
|
|
95
|
+
) {
|
|
96
|
+
sourceContext.responseData.contentType = value;
|
|
97
|
+
}
|
|
93
98
|
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
const sourceName = 'ClientRequest';
|
|
@@ -143,7 +148,6 @@ module.exports = function (core) {
|
|
|
143
148
|
}
|
|
144
149
|
});
|
|
145
150
|
|
|
146
|
-
|
|
147
151
|
//
|
|
148
152
|
// now track the rawHeaders. headers are complicated because they appear
|
|
149
153
|
// three times: headers, headersDistinct, and rawHeaders and we want to
|
|
@@ -20,7 +20,7 @@ const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
|
20
20
|
module.exports = function(core) {
|
|
21
21
|
const koaSources = core.assess.dataflow.sources.koaInstrumentation = {};
|
|
22
22
|
|
|
23
|
-
require('./
|
|
23
|
+
require('./koa')(core);
|
|
24
24
|
require('./koa-bodyparsers')(core);
|
|
25
25
|
require('./koa-multer')(core);
|
|
26
26
|
require('./koa-routers')(core);
|
|
@@ -30,58 +30,86 @@ module.exports = (core) => {
|
|
|
30
30
|
},
|
|
31
31
|
} = core;
|
|
32
32
|
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
function postFn(name) {
|
|
34
|
+
return function(data) {
|
|
35
|
+
data.result = patcher.patch(data.result, {
|
|
36
36
|
name,
|
|
37
37
|
patchType,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
sourceContext.parsedBody = !!Object.keys(ctx.request.body).length;
|
|
75
|
-
} catch (err) {
|
|
76
|
-
logger.error({ err, inputType, funcKey }, 'unable to handle Koa source');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
await origNext(origErr);
|
|
80
|
-
};
|
|
38
|
+
pre(data) {
|
|
39
|
+
const { funcKey } = data;
|
|
40
|
+
const [ctx, origNext] = data.args;
|
|
41
|
+
const sourceContext = getSourceContext();
|
|
42
|
+
|
|
43
|
+
if (!sourceContext) return;
|
|
44
|
+
|
|
45
|
+
if (sourceContext.parsedBody) {
|
|
46
|
+
logger.trace({ funcKey }, 'values already tracked');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
data.args[1] = async function contrastNext(origErr) {
|
|
51
|
+
const contentType = scopes.sources.getStore()?.sourceInfo?.contentType;
|
|
52
|
+
const inputType = contentType?.includes?.('/json')
|
|
53
|
+
? InputType.JSON_VALUE
|
|
54
|
+
: typeof ctx.request.body == 'object'
|
|
55
|
+
? InputType.PARAMETER_VALUE
|
|
56
|
+
: InputType.BODY;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
sources.handle({
|
|
60
|
+
context: 'ctx.request.body',
|
|
61
|
+
name,
|
|
62
|
+
inputType,
|
|
63
|
+
stacktraceOpts: {
|
|
64
|
+
constructorOpt: contrastNext,
|
|
65
|
+
},
|
|
66
|
+
data: ctx.request.body,
|
|
67
|
+
sourceContext
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
sourceContext.parsedBody = !!Object.keys(ctx.request.body || {}).length;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
logger.error({ err, inputType, funcKey }, 'unable to handle Koa source');
|
|
81
73
|
}
|
|
82
|
-
|
|
74
|
+
|
|
75
|
+
await origNext(origErr);
|
|
76
|
+
};
|
|
83
77
|
}
|
|
84
|
-
})
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function install() {
|
|
83
|
+
|
|
84
|
+
[['koa-body', '>=4 <6'], ['koa-bodyparser', '>=4 <5']].forEach(([name, version]) => {
|
|
85
|
+
depHooks.resolve({ name, version }, (koaBody) =>
|
|
86
|
+
patcher.patch(koaBody, {
|
|
87
|
+
name,
|
|
88
|
+
patchType,
|
|
89
|
+
post: postFn(name)
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
depHooks.resolve({ name: 'koa-body', version: '>=6 <7' }, (koaBody) =>
|
|
95
|
+
patcher.patch(koaBody, 'koaBody', {
|
|
96
|
+
name: 'koaBody',
|
|
97
|
+
patchType,
|
|
98
|
+
post: postFn('koa-body')
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
depHooks.resolve({ name: '@koa/bodyparser', version: '>=5 <7' }, (koaBody) => {
|
|
103
|
+
const patchedBodyParser = patcher.patch(koaBody.bodyParser, {
|
|
104
|
+
name: '@koa/bodyparser',
|
|
105
|
+
patchType,
|
|
106
|
+
post: postFn('@koa/bodyparser')
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
return {
|
|
110
|
+
default: patchedBodyParser,
|
|
111
|
+
bodyParser: patchedBodyParser
|
|
112
|
+
};
|
|
85
113
|
});
|
|
86
114
|
}
|
|
87
115
|
|
|
@@ -67,7 +67,7 @@ module.exports = (core) => {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function install() {
|
|
70
|
-
[['koa-multer', '<2'], ['@koa/multer', '<
|
|
70
|
+
[['koa-multer', '<2'], ['@koa/multer', '>=3 <5']].forEach(([name, version]) => {
|
|
71
71
|
depHooks.resolve(
|
|
72
72
|
{ name, version }, (_export) => {
|
|
73
73
|
const origMulter = _export;
|
|
@@ -31,11 +31,11 @@ module.exports = (core) => {
|
|
|
31
31
|
|
|
32
32
|
// Patch `koa-router` and `@koa/router` to handle parsed params
|
|
33
33
|
function install() {
|
|
34
|
-
[['koa-router', '<
|
|
34
|
+
[['koa-router', '>=12 <15'], ['@koa/router', '>=12 <15']].forEach(([router, version]) => {
|
|
35
35
|
depHooks.resolve(
|
|
36
36
|
{ name: router, version, file: 'lib/layer.js' },
|
|
37
37
|
(layer) => {
|
|
38
|
-
|
|
38
|
+
patcher.patch(layer.prototype, 'params', {
|
|
39
39
|
name: `[${router}].layer.prototype`,
|
|
40
40
|
patchType,
|
|
41
41
|
post({ orig, hooked, result, name, funcKey }) {
|
|
@@ -40,7 +40,7 @@ module.exports = (core) => {
|
|
|
40
40
|
* registers a depHook for koa module instrumentation
|
|
41
41
|
*/
|
|
42
42
|
function install() {
|
|
43
|
-
depHooks.resolve({ name: 'koa', version: '>=2.3.0 <
|
|
43
|
+
depHooks.resolve({ name: 'koa', version: '>=2.3.0 <4' }, (Koa) => {
|
|
44
44
|
const createMiddleware = ({ name, funcKey }) => {
|
|
45
45
|
const contrastStartMiddleware = function contrastStartMiddleware(ctx, next) {
|
|
46
46
|
const sourceContext = getSourceContext();
|
|
@@ -101,9 +101,9 @@ module.exports = (core) => {
|
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const
|
|
104
|
+
const koaInstrumentation = sources.koaInstrumentation.koa = {
|
|
105
105
|
install
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
-
return
|
|
108
|
+
return koaInstrumentation;
|
|
109
109
|
};
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { InputType, set } = require('@contrast/common');
|
|
18
|
+
const Core = require('@contrast/core/lib/ioc/core');
|
|
19
|
+
const { patchType } = require('../common');
|
|
20
|
+
|
|
21
|
+
const COMPONENT_NAME = 'assess.dataflow.sources.socketIoInstrumentation';
|
|
22
|
+
|
|
23
|
+
module.exports = Core.makeComponent({
|
|
24
|
+
name: COMPONENT_NAME,
|
|
25
|
+
factory: (core) => new SocketIOAssessSource(core),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
class SocketIOAssessSource {
|
|
29
|
+
constructor(core) {
|
|
30
|
+
Object.defineProperty(this, 'core', { value: core });
|
|
31
|
+
set(core, COMPONENT_NAME, this);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Deploys socket.io instrumentation.
|
|
35
|
+
*/
|
|
36
|
+
install() {
|
|
37
|
+
const {
|
|
38
|
+
depHooks,
|
|
39
|
+
patcher,
|
|
40
|
+
assess,
|
|
41
|
+
} = this.core;
|
|
42
|
+
|
|
43
|
+
depHooks.resolve(
|
|
44
|
+
{ name: 'socket.io', version: '4' },
|
|
45
|
+
/**
|
|
46
|
+
* @param {import('socket.io-4')} xport the exported socket.io module
|
|
47
|
+
*/
|
|
48
|
+
(xport) => {
|
|
49
|
+
patcher.patch(xport.Socket.prototype, 'dispatch', {
|
|
50
|
+
name: 'socket.io.Socket.prototype.dispatch',
|
|
51
|
+
patchType,
|
|
52
|
+
pre(data) {
|
|
53
|
+
if (!Array.isArray(data.args[0])) return;
|
|
54
|
+
|
|
55
|
+
const sourceContext = assess.getSourceContext();
|
|
56
|
+
if (!sourceContext) return;
|
|
57
|
+
|
|
58
|
+
const [eventName, ...params] = data.args[0];
|
|
59
|
+
assess.dataflow.sources.handle({
|
|
60
|
+
data: params,
|
|
61
|
+
name: 'socket.io.Socket.prototype.dispatch',
|
|
62
|
+
inputType: InputType.WEBSOCKET,
|
|
63
|
+
stacktraceOpts: { constructorOpt: data.hooked },
|
|
64
|
+
sourceContext,
|
|
65
|
+
onEvent(event, fieldName, pathName) {
|
|
66
|
+
event.context = `socket.io Socket.on("${eventName}", ...params)`;
|
|
67
|
+
event.args = [{
|
|
68
|
+
tracked: true,
|
|
69
|
+
value: `params.${pathName}`,
|
|
70
|
+
}];
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
data.args[0] = [eventName, ...params];
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
13
|
* way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
|
-
import { Rule,
|
|
15
|
+
import { Rule, ConfigurationRule } from '@contrast/common';
|
|
16
16
|
import { Config } from '@contrast/config';
|
|
17
17
|
import { Core as _Core } from '@contrast/core';
|
|
18
18
|
import { Deadzones } from '@contrast/deadzones';
|
|
@@ -61,8 +61,9 @@ export interface SessionRuleState {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export interface RuleState {
|
|
64
|
-
[
|
|
65
|
-
[
|
|
64
|
+
[ConfigurationRule.HTTPONLY]?: SessionRuleState,
|
|
65
|
+
[ConfigurationRule.SECURE_FLAG_MISSING]?: SessionRuleState,
|
|
66
|
+
[ConfigurationRule.GRAPHQL_INTROSPECTION]?: SessionRuleState,
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export interface Assess {
|
package/lib/index.js
CHANGED
|
@@ -70,7 +70,7 @@ module.exports = function assess(core) {
|
|
|
70
70
|
require('./dataflow')(core);
|
|
71
71
|
require('./crypto-analysis')(core);
|
|
72
72
|
require('./response-scanning')(core);
|
|
73
|
-
require('./
|
|
73
|
+
require('./configuration-analysis')(core);
|
|
74
74
|
|
|
75
75
|
// append async state to sources store when request-scope sources are created
|
|
76
76
|
sources.addHook('onSource', (ctx) => {
|
package/lib/policy.js
CHANGED
|
@@ -21,7 +21,7 @@ const {
|
|
|
21
21
|
InputType,
|
|
22
22
|
Rule,
|
|
23
23
|
ResponseScanningRule,
|
|
24
|
-
|
|
24
|
+
ConfigurationRule,
|
|
25
25
|
set,
|
|
26
26
|
primordials: { ArrayPrototypeJoin, RegExpPrototypeTest }
|
|
27
27
|
} = require('@contrast/common');
|
|
@@ -30,7 +30,7 @@ const { Core } = require('@contrast/core/lib/ioc/core');
|
|
|
30
30
|
const ASSESS_RULES = Object.values({
|
|
31
31
|
...Rule,
|
|
32
32
|
...ResponseScanningRule,
|
|
33
|
-
...
|
|
33
|
+
...ConfigurationRule,
|
|
34
34
|
});
|
|
35
35
|
const BROAD_INPUT_EXCLUSION_TYPES = [
|
|
36
36
|
ExclusionType.BODY,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/assess",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.65.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)",
|
|
@@ -20,18 +20,18 @@
|
|
|
20
20
|
"test": "bash ../scripts/test.sh"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@contrast/common": "1.
|
|
24
|
-
"@contrast/config": "1.
|
|
25
|
-
"@contrast/core": "1.
|
|
26
|
-
"@contrast/dep-hooks": "1.
|
|
23
|
+
"@contrast/common": "1.38.0",
|
|
24
|
+
"@contrast/config": "1.54.0",
|
|
25
|
+
"@contrast/core": "1.59.0",
|
|
26
|
+
"@contrast/dep-hooks": "1.28.0",
|
|
27
27
|
"@contrast/distringuish": "^6.0.2",
|
|
28
|
-
"@contrast/instrumentation": "1.
|
|
29
|
-
"@contrast/logger": "1.
|
|
30
|
-
"@contrast/patcher": "1.
|
|
31
|
-
"@contrast/rewriter": "1.
|
|
32
|
-
"@contrast/route-coverage": "1.
|
|
33
|
-
"@contrast/scopes": "1.
|
|
34
|
-
"@contrast/sources": "1.
|
|
28
|
+
"@contrast/instrumentation": "1.38.0",
|
|
29
|
+
"@contrast/logger": "1.32.0",
|
|
30
|
+
"@contrast/patcher": "1.31.0",
|
|
31
|
+
"@contrast/rewriter": "1.36.0",
|
|
32
|
+
"@contrast/route-coverage": "1.51.0",
|
|
33
|
+
"@contrast/scopes": "1.29.0",
|
|
34
|
+
"@contrast/sources": "1.5.0",
|
|
35
35
|
"semver": "^7.6.0"
|
|
36
36
|
}
|
|
37
37
|
}
|