@contrast/assess 1.64.0 → 1.66.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.
Files changed (25) hide show
  1. package/lib/{session-configuration → configuration-analysis}/common.js +1 -1
  2. package/lib/{session-configuration → configuration-analysis}/handlers.js +23 -10
  3. package/lib/{session-configuration → configuration-analysis}/index.js +6 -4
  4. package/lib/configuration-analysis/install/apollo-server.js +92 -0
  5. package/lib/{session-configuration → configuration-analysis}/install/express-session.js +2 -2
  6. package/lib/{session-configuration → configuration-analysis}/install/fastify-cookie.js +2 -2
  7. package/lib/configuration-analysis/install/graphql-yoga.js +90 -0
  8. package/lib/{session-configuration → configuration-analysis}/install/hapi.js +2 -2
  9. package/lib/{session-configuration → configuration-analysis}/install/koa.js +3 -3
  10. package/lib/dataflow/propagation/install/string/substring.js +1 -1
  11. package/lib/dataflow/sinks/install/fs.js +8 -15
  12. package/lib/dataflow/sources/handler.js +9 -2
  13. package/lib/dataflow/sources/index.js +2 -0
  14. package/lib/dataflow/sources/install/fastify-websocket.js +63 -0
  15. package/lib/dataflow/sources/install/http.js +42 -38
  16. package/lib/dataflow/sources/install/koa/index.js +1 -1
  17. package/lib/dataflow/sources/install/koa/koa-bodyparsers.js +76 -48
  18. package/lib/dataflow/sources/install/koa/koa-multer.js +1 -1
  19. package/lib/dataflow/sources/install/koa/koa-routers.js +2 -2
  20. package/lib/dataflow/sources/install/koa/{koa2.js → koa.js} +3 -3
  21. package/lib/dataflow/sources/install/socket.io.js +80 -0
  22. package/lib/index.d.ts +4 -3
  23. package/lib/index.js +1 -1
  24. package/lib/policy.js +2 -2
  25. package/package.json +12 -12
@@ -15,5 +15,5 @@
15
15
  'use strict';
16
16
 
17
17
  module.exports = {
18
- patchType: 'session-configuration'
18
+ patchType: 'configuration'
19
19
  };
@@ -17,15 +17,15 @@
17
17
 
18
18
  const {
19
19
  Event,
20
- SessionConfigurationRule,
20
+ ConfigurationRule,
21
21
  isString,
22
22
  } = require('@contrast/common');
23
23
 
24
- const { HTTPONLY, SECURE_FLAG_MISSING } = SessionConfigurationRule;
24
+ const { HTTPONLY, SECURE_FLAG_MISSING, GRAPHQL_INTROSPECTION } = ConfigurationRule;
25
25
 
26
26
  module.exports = function (core) {
27
27
  const {
28
- assess: { sessionConfiguration },
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 {SessionConfigurationRule} ruleId
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
- sessionConfiguration.reportFindings({
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
- sessionConfiguration.handleHttpOnly = function(sourceContext, cookie, sessionEvent) {
92
+ configurationAnalysis.handleHttpOnly = function(sourceContext, cookie, sessionEvent) {
93
93
  handle(HTTPONLY, sourceContext, cookie, sessionEvent);
94
94
  };
95
95
 
96
- sessionConfiguration.handleSecure = function (sourceContext, cookie, sessionEvent) {
96
+ configurationAnalysis.handleSecure = function (sourceContext, cookie, sessionEvent) {
97
97
  handle(SECURE_FLAG_MISSING, sourceContext, cookie, sessionEvent);
98
98
  };
99
99
 
100
- sessionConfiguration.reportFindings = function (finding) {
101
- messages.emit(Event.ASSESS_SESSION_CONFIGURATION_FINDING, finding);
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 sessionConfiguration;
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 sessionConfiguration = core.assess.sessionConfiguration = {};
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
- sessionConfiguration.install = function() {
30
- callChildComponentMethodsSync(sessionConfiguration, 'install');
31
+ configurationAnalysis.install = function() {
32
+ callChildComponentMethodsSync(configurationAnalysis, 'install');
31
33
  };
32
34
 
33
- return sessionConfiguration;
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' }, (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
- sessionConfiguration: {
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.sessionConfiguration.expressSession = {};
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
- sessionConfiguration: {
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.sessionConfiguration.fastifyCookie = {
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: '*' }, (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
- sessionConfiguration: {
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.sessionConfiguration.hapiSession = {};
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
- sessionConfiguration: {
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.sessionConfiguration.koa = {
40
+ return core.assess.configurationAnalysis.koa = {
41
41
  install () {
42
- depHooks.resolve({ name: 'koa', version: '>=2.3.0 <3' }, (Koa) => {
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: 'prototype.substring',
92
+ methodName: `prototype.${method}`,
93
93
  get context() {
94
94
  return `'${objInfo.value}'.substring(${ArrayPrototypeJoin.call(args.map(a => a.value))})`;
95
95
  },
@@ -41,6 +41,7 @@ module.exports = function(core) {
41
41
  tracker,
42
42
  sinks: { isVulnerable, reportFindings },
43
43
  },
44
+ ruleScopes
44
45
  },
45
46
  } = core;
46
47
 
@@ -60,12 +61,12 @@ module.exports = function(core) {
60
61
  }, []);
61
62
  }
62
63
 
63
- const pre = (name, method, moduleName = 'fs', fullMethodName = '') => (data) => {
64
+ const around = (name, method, moduleName = 'fs', fullMethodName = '') => (next, data) => {
64
65
  const { name: methodName, indices } = method;
65
- if (!getSinkContext(ruleId)) return;
66
+ if (!getSinkContext(ruleId)) return next();
66
67
 
67
68
  const values = getValues(indices, data.args);
68
- if (!values.length) return;
69
+ if (!values.length) return next();
69
70
 
70
71
  const args = values.map((v) => {
71
72
  const strInfo = tracker.getData(v);
@@ -111,6 +112,7 @@ module.exports = function(core) {
111
112
  });
112
113
  }
113
114
  }
115
+ return ruleScopes.run(ruleId, next);
114
116
  };
115
117
 
116
118
  core.assess.dataflow.sinks.pathTraversal = {
@@ -123,7 +125,7 @@ module.exports = function(core) {
123
125
  patcher.patch(fs, method.name, {
124
126
  name,
125
127
  patchType,
126
- pre: pre(name, method),
128
+ around: around(name, method),
127
129
  });
128
130
  }
129
131
 
@@ -134,19 +136,10 @@ module.exports = function(core) {
134
136
  patcher.patch(fs, syncName, {
135
137
  name,
136
138
  patchType,
137
- pre: pre(name, method, 'fs', syncName),
139
+ around: around(name, method, 'fs', syncName),
138
140
  });
139
141
  }
140
142
  }
141
-
142
- if (method.promises && fs.promises && fs.promises[method.name]) {
143
- const name = `fs.promises.${method.name}`;
144
- patcher.patch(fs.promises, method.name, {
145
- name,
146
- patchType,
147
- pre: pre(name, method, 'fs.promises'),
148
- });
149
- }
150
143
  }
151
144
  });
152
145
 
@@ -157,7 +150,7 @@ module.exports = function(core) {
157
150
  patcher.patch(fsPromises, method.name, {
158
151
  name,
159
152
  patchType,
160
- pre: pre(name, method, 'fsPromises'),
153
+ around: around(name, method, 'fsPromises'),
161
154
  });
162
155
  }
163
156
  }
@@ -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
- return eventFactory.createSourceEvent({
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
- * invokes the protect service to do analysis when appropriate.
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, res] = data.args;
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
- patcher.patch(res, 'writeHead', {
56
- name: 'write-head',
57
- patchType,
58
- pre(data) {
59
- const obj = data.args[data.args.length - 1];
60
- if (!obj) return;
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
- if (Array.isArray(obj)) {
63
- for (let i = 0; i < obj.length; i += 2) {
64
- const key = obj[i];
65
- const value = obj[i + 1];
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
- if (StringPrototypeToLowerCase.call(key) === 'content-type') {
68
- sourceContext.responseData.contentType = value;
71
+ if (StringPrototypeToLowerCase.call(key) === 'content-type') {
72
+ sourceContext.responseData.contentType = value;
73
+ }
69
74
  }
70
- }
71
- } else if (typeof obj === 'object') {
72
- for (const [key, value] of Object.entries(obj)) {
73
- if (StringPrototypeToLowerCase.call(key) === 'content-type') {
74
- sourceContext.responseData.contentType = value;
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
- if (!patcher.hooks.get(res?.setHeader)?.funcKeys.has(`${patchType}:set-header`)) {
82
- patcher.patch(res, 'setHeader', {
83
- name: 'set-header',
84
- patchType,
85
- pre(data) {
86
- const [name = '', value] = data.args;
87
- if (
88
- value &&
89
- StringPrototypeToLowerCase.call(name) === 'content-type' &&
90
- getSourceContext()
91
- ) {
92
- sourceContext.responseData.contentType = value;
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('./koa2')(core);
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 install() {
34
- [['koa-body', '<7'], ['koa-bodyparser', '<5']].forEach(([name, version]) => {
35
- depHooks.resolve({ name, version }, (koaBody) => patcher.patch(koaBody, {
33
+ function postFn(name) {
34
+ return function(data) {
35
+ data.result = patcher.patch(data.result, {
36
36
  name,
37
37
  patchType,
38
- post(data) {
39
- data.result = patcher.patch(data.result, {
40
- name,
41
- patchType,
42
- pre(data) {
43
- const { funcKey } = data;
44
- const [ctx, origNext] = data.args;
45
- const sourceContext = getSourceContext();
46
-
47
- if (!sourceContext) return;
48
-
49
- if (sourceContext.parsedBody) {
50
- logger.trace({ funcKey }, 'values already tracked');
51
- return;
52
- }
53
-
54
- data.args[1] = async function contrastNext(origErr) {
55
- const contentType = scopes.sources.getStore()?.sourceInfo?.contentType;
56
- const inputType = contentType?.includes?.('/json')
57
- ? InputType.JSON_VALUE
58
- : typeof ctx.request.body == 'object'
59
- ? InputType.PARAMETER_VALUE
60
- : InputType.BODY;
61
-
62
- try {
63
- sources.handle({
64
- context: 'ctx.request.body',
65
- name,
66
- inputType,
67
- stacktraceOpts: {
68
- constructorOpt: contrastNext,
69
- },
70
- data: ctx.request.body,
71
- sourceContext
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', '<4']].forEach(([name, version]) => {
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', '<14'], ['@koa/router', '<14']].forEach(([router, version]) => {
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
- layer.prototype = patcher.patch(layer.prototype, 'params', {
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 <3' }, (Koa) => {
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 koa2Instrumentation = sources.koaInstrumentation.koa2 = {
104
+ const koaInstrumentation = sources.koaInstrumentation.koa = {
105
105
  install
106
106
  };
107
107
 
108
- return koa2Instrumentation;
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, SessionConfigurationRule } from '@contrast/common';
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
- [SessionConfigurationRule.HTTPONLY]?: SessionRuleState,
65
- [SessionConfigurationRule.SECURE_FLAG_MISSING]?: SessionRuleState,
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('./session-configuration')(core);
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
- SessionConfigurationRule,
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
- ...SessionConfigurationRule,
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.64.0",
3
+ "version": "1.66.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.37.0",
24
- "@contrast/config": "1.53.0",
25
- "@contrast/core": "1.58.0",
26
- "@contrast/dep-hooks": "1.27.0",
23
+ "@contrast/common": "1.38.0",
24
+ "@contrast/config": "1.54.1",
25
+ "@contrast/core": "1.59.1",
26
+ "@contrast/dep-hooks": "1.28.1",
27
27
  "@contrast/distringuish": "^6.0.2",
28
- "@contrast/instrumentation": "1.37.0",
29
- "@contrast/logger": "1.31.0",
30
- "@contrast/patcher": "1.30.0",
31
- "@contrast/rewriter": "1.35.0",
32
- "@contrast/route-coverage": "1.50.0",
33
- "@contrast/scopes": "1.28.0",
34
- "@contrast/sources": "1.4.0",
28
+ "@contrast/instrumentation": "1.38.1",
29
+ "@contrast/logger": "1.32.1",
30
+ "@contrast/patcher": "1.31.1",
31
+ "@contrast/rewriter": "1.36.1",
32
+ "@contrast/route-coverage": "1.52.0",
33
+ "@contrast/scopes": "1.29.1",
34
+ "@contrast/sources": "1.5.1",
35
35
  "semver": "^7.6.0"
36
36
  }
37
37
  }