@contrast/assess 1.18.0 → 1.20.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 (160) hide show
  1. package/LICENSE +1 -1
  2. package/lib/constants.js +26 -0
  3. package/lib/crypto-analysis/common.js +20 -0
  4. package/lib/crypto-analysis/index.js +44 -0
  5. package/lib/crypto-analysis/install/crypto.js +156 -0
  6. package/lib/crypto-analysis/install/math.js +104 -0
  7. package/lib/dataflow/index.js +1 -1
  8. package/lib/dataflow/propagation/common.js +1 -1
  9. package/lib/dataflow/propagation/index.js +1 -1
  10. package/lib/dataflow/propagation/install/JSON/index.js +1 -1
  11. package/lib/dataflow/propagation/install/JSON/parse-fn.js +1 -1
  12. package/lib/dataflow/propagation/install/JSON/parse.js +15 -14
  13. package/lib/dataflow/propagation/install/JSON/stringify.js +2 -2
  14. package/lib/dataflow/propagation/install/array-prototype-join.js +1 -1
  15. package/lib/dataflow/propagation/install/buffer.js +1 -1
  16. package/lib/dataflow/propagation/install/contrast-methods/add.js +1 -1
  17. package/lib/dataflow/propagation/install/contrast-methods/index.js +1 -1
  18. package/lib/dataflow/propagation/install/contrast-methods/number.js +4 -3
  19. package/lib/dataflow/propagation/install/contrast-methods/string.js +1 -1
  20. package/lib/dataflow/propagation/install/contrast-methods/tag.js +1 -1
  21. package/lib/dataflow/propagation/install/decode-uri-component.js +1 -1
  22. package/lib/dataflow/propagation/install/ejs/escape-xml.js +3 -3
  23. package/lib/dataflow/propagation/install/ejs/index.js +2 -1
  24. package/lib/dataflow/propagation/install/ejs/template.js +79 -0
  25. package/lib/dataflow/propagation/install/encode-uri.js +1 -1
  26. package/lib/dataflow/propagation/install/escape-html.js +1 -1
  27. package/lib/dataflow/propagation/install/escape.js +1 -1
  28. package/lib/dataflow/propagation/install/handlebars-utils-escape-expression.js +1 -1
  29. package/lib/dataflow/propagation/install/isnumeric-0.js +3 -3
  30. package/lib/dataflow/propagation/install/joi/any.js +1 -1
  31. package/lib/dataflow/propagation/install/joi/boolean.js +1 -1
  32. package/lib/dataflow/propagation/install/joi/expression.js +1 -1
  33. package/lib/dataflow/propagation/install/joi/index.js +1 -1
  34. package/lib/dataflow/propagation/install/joi/keys.js +1 -1
  35. package/lib/dataflow/propagation/install/joi/number.js +1 -1
  36. package/lib/dataflow/propagation/install/joi/object.js +1 -1
  37. package/lib/dataflow/propagation/install/joi/string-schema.js +1 -1
  38. package/lib/dataflow/propagation/install/joi/utils.js +1 -1
  39. package/lib/dataflow/propagation/install/joi/values.js +1 -1
  40. package/lib/dataflow/propagation/install/mongoose/common.js +1 -1
  41. package/lib/dataflow/propagation/install/mongoose/index.js +1 -1
  42. package/lib/dataflow/propagation/install/mongoose/schema-map.js +1 -1
  43. package/lib/dataflow/propagation/install/mongoose/schema-mixed.js +1 -1
  44. package/lib/dataflow/propagation/install/mongoose/schema-string.js +1 -1
  45. package/lib/dataflow/propagation/install/mustache-escape.js +1 -1
  46. package/lib/dataflow/propagation/install/mysql-connection-escape.js +1 -1
  47. package/lib/dataflow/propagation/install/parse-int.js +3 -3
  48. package/lib/dataflow/propagation/install/path/basename.js +1 -1
  49. package/lib/dataflow/propagation/install/path/common.js +1 -1
  50. package/lib/dataflow/propagation/install/path/dirname.js +1 -1
  51. package/lib/dataflow/propagation/install/path/extname.js +1 -1
  52. package/lib/dataflow/propagation/install/path/format.js +1 -1
  53. package/lib/dataflow/propagation/install/path/index.js +1 -1
  54. package/lib/dataflow/propagation/install/path/join-and-resolve.js +1 -1
  55. package/lib/dataflow/propagation/install/path/normalize.js +1 -1
  56. package/lib/dataflow/propagation/install/path/parse.js +1 -1
  57. package/lib/dataflow/propagation/install/path/relative.js +1 -1
  58. package/lib/dataflow/propagation/install/path/toNamespacedPath.js +1 -1
  59. package/lib/dataflow/propagation/install/pug/index.js +3 -3
  60. package/lib/dataflow/propagation/install/pug-runtime-escape.js +1 -1
  61. package/lib/dataflow/propagation/install/querystring/escape.js +1 -1
  62. package/lib/dataflow/propagation/install/querystring/index.js +1 -1
  63. package/lib/dataflow/propagation/install/querystring/parse.js +1 -1
  64. package/lib/dataflow/propagation/install/querystring/stringify.js +1 -1
  65. package/lib/dataflow/propagation/install/reg-exp-prototype-exec.js +1 -1
  66. package/lib/dataflow/propagation/install/send.js +1 -1
  67. package/lib/dataflow/propagation/install/sequelize/index.js +1 -1
  68. package/lib/dataflow/propagation/install/sequelize/query-generator.js +1 -1
  69. package/lib/dataflow/propagation/install/sequelize/sql-string.js +1 -1
  70. package/lib/dataflow/propagation/install/sql-template-strings.js +1 -1
  71. package/lib/dataflow/propagation/install/string/concat.js +1 -1
  72. package/lib/dataflow/propagation/install/string/format-methods.js +1 -1
  73. package/lib/dataflow/propagation/install/string/html-methods.js +1 -1
  74. package/lib/dataflow/propagation/install/string/index.js +1 -1
  75. package/lib/dataflow/propagation/install/string/match-all.js +1 -1
  76. package/lib/dataflow/propagation/install/string/match.js +1 -1
  77. package/lib/dataflow/propagation/install/string/replace.js +1 -1
  78. package/lib/dataflow/propagation/install/string/slice.js +1 -1
  79. package/lib/dataflow/propagation/install/string/split.js +1 -1
  80. package/lib/dataflow/propagation/install/string/substring.js +1 -1
  81. package/lib/dataflow/propagation/install/string/trim.js +1 -1
  82. package/lib/dataflow/propagation/install/unescape.js +1 -1
  83. package/lib/dataflow/propagation/install/url/domain-parsers.js +1 -1
  84. package/lib/dataflow/propagation/install/url/index.js +1 -1
  85. package/lib/dataflow/propagation/install/url/parse.js +1 -1
  86. package/lib/dataflow/propagation/install/url/searchParams.js +1 -1
  87. package/lib/dataflow/propagation/install/url/url.js +1 -1
  88. package/lib/dataflow/propagation/install/util-format.js +10 -4
  89. package/lib/dataflow/propagation/install/validator/hooks.js +1 -1
  90. package/lib/dataflow/propagation/install/validator/index.js +1 -1
  91. package/lib/dataflow/propagation/install/validator/methods.js +1 -1
  92. package/lib/dataflow/sinks/common.js +1 -1
  93. package/lib/dataflow/sinks/index.js +1 -1
  94. package/lib/dataflow/sinks/install/child-process.js +21 -15
  95. package/lib/dataflow/sinks/install/eval.js +17 -15
  96. package/lib/dataflow/sinks/install/express/index.js +1 -1
  97. package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +15 -9
  98. package/lib/dataflow/sinks/install/fastify/index.js +1 -1
  99. package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +13 -6
  100. package/lib/dataflow/sinks/install/fs.js +8 -8
  101. package/lib/dataflow/sinks/install/function.js +9 -13
  102. package/lib/dataflow/sinks/install/http/index.js +1 -1
  103. package/lib/dataflow/sinks/install/http/request.js +17 -9
  104. package/lib/dataflow/sinks/install/http/server-response.js +12 -3
  105. package/lib/dataflow/sinks/install/koa/index.js +1 -1
  106. package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +16 -9
  107. package/lib/dataflow/sinks/install/libxmljs.js +16 -11
  108. package/lib/dataflow/sinks/install/marsdb.js +17 -12
  109. package/lib/dataflow/sinks/install/mongodb.js +32 -22
  110. package/lib/dataflow/sinks/install/mssql.js +21 -10
  111. package/lib/dataflow/sinks/install/mysql.js +16 -9
  112. package/lib/dataflow/sinks/install/node-serialize.js +16 -18
  113. package/lib/dataflow/sinks/install/postgres.js +18 -5
  114. package/lib/dataflow/sinks/install/sequelize.js +23 -17
  115. package/lib/dataflow/sinks/install/sqlite3.js +21 -8
  116. package/lib/dataflow/sinks/install/vm.js +20 -18
  117. package/lib/dataflow/sources/common.js +1 -1
  118. package/lib/dataflow/sources/handler.js +11 -10
  119. package/lib/dataflow/sources/index.js +2 -2
  120. package/lib/dataflow/sources/install/body-parser1.js +11 -13
  121. package/lib/dataflow/sources/install/{busboy1.js → busboy.js} +15 -15
  122. package/lib/dataflow/sources/install/cookie-parser1.js +7 -6
  123. package/lib/dataflow/sources/install/express/index.js +1 -1
  124. package/lib/dataflow/sources/install/express/params.js +9 -10
  125. package/lib/dataflow/sources/install/express/parsedUrl.js +1 -1
  126. package/lib/dataflow/sources/install/fastify/fastify.js +6 -7
  127. package/lib/dataflow/sources/install/fastify/index.js +1 -1
  128. package/lib/dataflow/sources/install/formidable1.js +8 -6
  129. package/lib/dataflow/sources/install/http.js +17 -45
  130. package/lib/dataflow/sources/install/koa/index.js +2 -1
  131. package/lib/dataflow/sources/install/koa/koa-bodyparsers.js +10 -9
  132. package/lib/dataflow/sources/install/koa/koa-multer.js +102 -0
  133. package/lib/dataflow/sources/install/koa/koa-routers.js +6 -8
  134. package/lib/dataflow/sources/install/koa/koa2.js +42 -38
  135. package/lib/dataflow/sources/install/multer1.js +26 -52
  136. package/lib/dataflow/sources/install/qs6.js +7 -6
  137. package/lib/dataflow/sources/install/querystring.js +5 -8
  138. package/lib/dataflow/tag-utils.js +1 -1
  139. package/lib/dataflow/tracker.js +1 -1
  140. package/lib/dataflow/utils/is-safe-content-type.js +1 -1
  141. package/lib/dataflow/utils/is-vulnerable.js +1 -1
  142. package/lib/event-factory.js +75 -26
  143. package/lib/get-policy.js +68 -0
  144. package/lib/get-source-context.js +62 -0
  145. package/lib/index.d.ts +64 -0
  146. package/lib/index.js +21 -20
  147. package/lib/make-source-context.js +78 -0
  148. package/lib/response-scanning/handlers/index.js +56 -29
  149. package/lib/response-scanning/handlers/utils.js +1 -1
  150. package/lib/response-scanning/index.js +1 -1
  151. package/lib/response-scanning/install/http.js +14 -8
  152. package/lib/rule-scopes.js +48 -0
  153. package/lib/session-configuration/common.js +1 -1
  154. package/lib/session-configuration/handlers.js +66 -47
  155. package/lib/session-configuration/index.js +3 -1
  156. package/lib/session-configuration/install/express-session.js +23 -26
  157. package/lib/session-configuration/install/fastify-cookie.js +110 -0
  158. package/lib/session-configuration/install/hapi.js +8 -11
  159. package/lib/session-configuration/install/koa.js +101 -0
  160. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2023 Contrast Security, Inc
2
+ * Copyright: 2024 Contrast Security, Inc
3
3
  * Contact: support@contrastsecurity.com
4
4
  * License: Commercial
5
5
 
@@ -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 (!isHtmlContent(responseHeaders)) {
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 (isParseableResponse(responseHeaders) && !responseBody) {
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
- * Checks the response headers for the CSP. If found, and insecure, will return
158
- * the evidence for reporting.
159
- *
160
- * @param {Object} responseHeaders - HTTP headers object.
161
- * @returns {Object} - Evidence for insecure CSP header.
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
- data: JSON.stringify(instructions)
281
+ platform: header,
260
282
  }
261
283
  });
262
284
  }
263
285
  };
264
286
 
265
287
  responseScanning.handleXxsProtectionHeaderDisabled = function(sourceContext, { responseHeaders }) {
266
- const header = responseHeaders['x-xss-protection'];
288
+ if (!isEnabled(XXSPROTECTION_HEADER_DISABLED, sourceContext)) return;
267
289
 
268
- if (header?.startsWith?.('1')) return;
290
+ const header = responseHeaders['x-xss-protection'];
269
291
 
270
- reportFindings(sourceContext, {
271
- ruleId: ResponseScanningRule.XXSPROTECTION_HEADER_DISABLED,
272
- vulnerabilityMetadata: {
273
- data: header,
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
-
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2023 Contrast Security, Inc
2
+ * Copyright: 2024 Contrast Security, Inc
3
3
  * Contact: support@contrastsecurity.com
4
4
  * License: Commercial
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2023 Contrast Security, Inc
2
+ * Copyright: 2024 Contrast Security, Inc
3
3
  * Contact: support@contrastsecurity.com
4
4
  * License: Commercial
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2023 Contrast Security, Inc
2
+ * Copyright: 2024 Contrast Security, Inc
3
3
  * Contact: support@contrastsecurity.com
4
4
  * License: Commercial
5
5
 
@@ -17,12 +17,17 @@
17
17
 
18
18
  const { split, substring, toLowerCase, trim } = require('@contrast/common');
19
19
 
20
+ /**
21
+ * @param {{
22
+ * assess: import('@contrast/assess').Assess,
23
+ * }} core
24
+ */
20
25
  module.exports = function(core) {
21
26
  const {
22
27
  depHooks,
23
28
  patcher,
24
- scopes: { sources },
25
29
  assess: {
30
+ getSourceContext,
26
31
  responseScanning: {
27
32
  handleAutoCompleteMissing,
28
33
  handleCacheControlsMissing,
@@ -36,6 +41,7 @@ module.exports = function(core) {
36
41
  }
37
42
  }
38
43
  } = core;
44
+
39
45
  const http = core.assess.responseScanning.httpInstrumentation = {};
40
46
 
41
47
  function parseHeaders(rawHeaders) {
@@ -90,7 +96,7 @@ module.exports = function(core) {
90
96
  name,
91
97
  patchType,
92
98
  post(data) {
93
- const sourceContext = sources.getStore()?.assess;
99
+ const sourceContext = getSourceContext();
94
100
  if (!sourceContext) return;
95
101
 
96
102
  const evaluationContext = {
@@ -108,7 +114,7 @@ module.exports = function(core) {
108
114
  name,
109
115
  patchType,
110
116
  post(data) {
111
- const sourceContext = sources.getStore()?.assess;
117
+ const sourceContext = getSourceContext();
112
118
  if (!sourceContext) return;
113
119
 
114
120
  const evaluationContext = {
@@ -130,7 +136,7 @@ module.exports = function(core) {
130
136
  name,
131
137
  patchType,
132
138
  post(data) {
133
- const sourceContext = sources.getStore()?.assess;
139
+ const sourceContext = getSourceContext();
134
140
  if (!sourceContext) return;
135
141
 
136
142
  const headersSymbol = Object.getOwnPropertySymbols(data.obj).find(symbol => symbol.toString().includes('headers'));
@@ -149,7 +155,7 @@ module.exports = function(core) {
149
155
  name,
150
156
  patchType,
151
157
  post(data) {
152
- const sourceContext = sources.getStore()?.assess;
158
+ const sourceContext = getSourceContext();
153
159
  if (!sourceContext) return;
154
160
 
155
161
  const headersSymbol = Object.getOwnPropertySymbols(data.result).find(symbol => symbol.toString().includes('headers'));
@@ -182,7 +188,7 @@ module.exports = function(core) {
182
188
  name: 'Http2Stream.write',
183
189
  patchType,
184
190
  post(data) {
185
- const sourceContext = sources.getStore()?.assess;
191
+ const sourceContext = getSourceContext();
186
192
  if (!sourceContext) return;
187
193
 
188
194
  const evaluationContext = {
@@ -198,7 +204,7 @@ module.exports = function(core) {
198
204
  name: 'Http2Stream.end',
199
205
  patchType,
200
206
  post(data) {
201
- const sourceContext = sources.getStore()?.assess;
207
+ const sourceContext = getSourceContext();
202
208
  if (!sourceContext) return;
203
209
 
204
210
  const evaluationContext = {
@@ -0,0 +1,48 @@
1
+ /*
2
+ * Copyright: 2024 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 { AsyncLocalStorage } = require('async_hooks');
19
+ /**
20
+ * @param {Object} core
21
+ * @param {import('@contrast/assess').Assess} core.assess
22
+ */
23
+ module.exports = function (core) {
24
+
25
+ /** @type {Map<Rule,AsyncLocalStorage>} */
26
+ const scopes = new Map();
27
+ /** @type {{locked:boolean}}*/
28
+ const store = Object.freeze({ locked: true });
29
+
30
+ return core.assess.ruleScopes = {
31
+ /**
32
+ * @param {import('@contrast/common').Rule} ruleId
33
+ * @param {functionn} cb
34
+ */
35
+ run(ruleId, cb) {
36
+ if (!scopes.has(ruleId)) scopes.set(ruleId, new AsyncLocalStorage());
37
+ return scopes.get(ruleId).run(store, cb);
38
+ },
39
+
40
+ /**
41
+ * @param {Rule} ruleId
42
+ */
43
+ isLocked(ruleId) {
44
+ return !!scopes.get(ruleId)?.getStore?.()?.locked;
45
+ }
46
+ };
47
+
48
+ };
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2023 Contrast Security, Inc
2
+ * Copyright: 2024 Contrast Security, Inc
3
3
  * Contact: support@contrastsecurity.com
4
4
  * License: Commercial
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2023 Contrast Security, Inc
2
+ * Copyright: 2024 Contrast Security, Inc
3
3
  * Contact: support@contrastsecurity.com
4
4
  * License: Commercial
5
5
 
@@ -17,69 +17,88 @@
17
17
 
18
18
  const {
19
19
  Event,
20
- SessionConfigurationRule: { HTTPONLY, SECURE_FLAG_MISSING },
20
+ SessionConfigurationRule,
21
+ isString,
21
22
  } = require('@contrast/common');
22
23
 
24
+ const { HTTPONLY, SECURE_FLAG_MISSING } = SessionConfigurationRule;
25
+
23
26
  module.exports = function (core) {
24
27
  const {
25
28
  assess: { sessionConfiguration },
26
29
  messages,
27
30
  } = core;
28
31
 
29
- const checkCookieValue = (ruleId, sinkEvent, cookieValue, sourceContext) => {
30
- if (cookieValue.includes(ruleId === HTTPONLY ? 'httponly' : 'secure')) {
31
- return;
32
+ function* ensureIterable(value) {
33
+ if (Array.isArray(value)) {
34
+ for (let i = 0; i < value.length; i++) {
35
+ yield value[i];
36
+ }
37
+ } else {
38
+ yield value;
32
39
  }
40
+ }
33
41
 
34
- sessionConfiguration.reportFindings(sourceContext, {
35
- ruleId,
36
- sinkEvent,
37
- properties: {
38
- evidence: cookieValue,
39
- },
40
- });
41
- };
42
-
43
- const handleCookie = (
44
- sourceContext,
45
- cookieValue,
46
- ruleId,
47
- sessionEvent
48
- ) => {
49
- if (Array.isArray(cookieValue)) {
50
- return cookieValue.forEach((value) =>
51
- checkCookieValue(ruleId, sessionEvent, value, sourceContext)
52
- );
42
+ /**
43
+ * @param {SessionConfigurationRule} ruleId
44
+ * @param {import('@contrast/assess').SourceContext} sourceContext
45
+ * @returns {import('@contrast/assess').SessionRuleState}
46
+ */
47
+ function ensureState(ruleId, sourceContext) {
48
+ if (!sourceContext.ruleState[ruleId]) {
49
+ sourceContext.ruleState[ruleId] = {
50
+ reported: false,
51
+ valuesAnalyzed: new Set(),
52
+ };
53
53
  }
54
+ return sourceContext.ruleState[ruleId];
55
+ }
54
56
 
55
- checkCookieValue(ruleId, sessionEvent, cookieValue, sourceContext);
56
- };
57
+ function isVulnerable(ruleId, value) {
58
+ if (!isString(value)) return false;
59
+
60
+ const search = ruleId === HTTPONLY ? 'HttpOnly;'
61
+ : ruleId === SECURE_FLAG_MISSING ? 'Secure;'
62
+ : undefined;
63
+
64
+ return search && value.indexOf(search) === -1;
65
+ }
66
+
67
+ function handle(ruleId, sourceContext, cookie, sessionEvent) {
68
+ const state = ensureState(ruleId, sourceContext);
69
+
70
+ if (!sourceContext?.policy?.enabledRules?.has?.(ruleId) || state.reported) return;
71
+
72
+ for (const value of ensureIterable(cookie)) {
73
+ if (state.valuesAnalyzed.has(value)) continue;
74
+
75
+ state.valuesAnalyzed.add(value);
76
+ if (!isVulnerable(ruleId, value)) continue;
77
+
78
+ else {
79
+ sessionConfiguration.reportFindings({
80
+ ruleId,
81
+ sinkEvent: sessionEvent,
82
+ properties: {
83
+ evidence: value,
84
+ },
85
+ });
86
+ state.reported = true;
87
+ break;
88
+ }
89
+ }
90
+ }
57
91
 
58
- sessionConfiguration.handleHttpOnly = function (
59
- sourceContext,
60
- cookieValue,
61
- sessionEvent
62
- ) {
63
- handleCookie(sourceContext, cookieValue, HTTPONLY, sessionEvent);
92
+ sessionConfiguration.handleHttpOnly = function(sourceContext, cookie, sessionEvent) {
93
+ handle(HTTPONLY, sourceContext, cookie, sessionEvent);
64
94
  };
65
95
 
66
- sessionConfiguration.handleSecure = function (
67
- sourceContext,
68
- cookieValue,
69
- sessionEvent
70
- ) {
71
- handleCookie(sourceContext, cookieValue, SECURE_FLAG_MISSING, sessionEvent);
96
+ sessionConfiguration.handleSecure = function (sourceContext, cookie, sessionEvent) {
97
+ handle(SECURE_FLAG_MISSING, sourceContext, cookie, sessionEvent);
72
98
  };
73
99
 
74
- // _sourceContext is unused
75
- sessionConfiguration.reportFindings = function (
76
- _sourceContext,
77
- vulnerabilityMetadata
78
- ) {
79
- messages.emit(
80
- Event.ASSESS_SESSION_CONFIGURATION_FINDING,
81
- vulnerabilityMetadata
82
- );
100
+ sessionConfiguration.reportFindings = function (finding) {
101
+ messages.emit(Event.ASSESS_SESSION_CONFIGURATION_FINDING, finding);
83
102
  };
84
103
 
85
104
  return sessionConfiguration;
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2023 Contrast Security, Inc
2
+ * Copyright: 2024 Contrast Security, Inc
3
3
  * Contact: support@contrastsecurity.com
4
4
  * License: Commercial
5
5
 
@@ -22,7 +22,9 @@ module.exports = function(core) {
22
22
 
23
23
  require('./handlers')(core);
24
24
  require('./install/express-session')(core);
25
+ require('./install/fastify-cookie')(core);
25
26
  require('./install/hapi')(core);
27
+ require('./install/koa')(core);
26
28
 
27
29
  sessionConfiguration.install = function() {
28
30
  callChildComponentMethodsSync(sessionConfiguration, 'install');
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2023 Contrast Security, Inc
2
+ * Copyright: 2024 Contrast Security, Inc
3
3
  * Contact: support@contrastsecurity.com
4
4
  * License: Commercial
5
5
 
@@ -18,9 +18,16 @@ const util = require('util');
18
18
  const { toLowerCase } = require('@contrast/common');
19
19
  const { patchType } = require('../common');
20
20
 
21
+ /**
22
+ * @param {{
23
+ * assess: import('@contrast/assess').Assess,
24
+ * scopes: import('@contrast/scopes').Scopes,
25
+ * }} core
26
+ */
21
27
  module.exports = function (core) {
22
28
  const {
23
29
  assess: {
30
+ getSourceContext,
24
31
  eventFactory: { createSessionEvent },
25
32
  sessionConfiguration: {
26
33
  handleHttpOnly,
@@ -29,7 +36,6 @@ module.exports = function (core) {
29
36
  },
30
37
  depHooks,
31
38
  patcher,
32
- scopes: { sources },
33
39
  } = core;
34
40
 
35
41
  const expressSession = core.assess.sessionConfiguration.expressSession = {};
@@ -44,36 +50,27 @@ module.exports = function (core) {
44
50
  patchType,
45
51
  post(data) {
46
52
  const options = data.args[0];
47
-
48
- // obfuscate the cookie secret
49
- if (Array.isArray(data.args) && data.args[0] && data.args[0].secret) {
50
- data.args[0].secret = '[HIDDEN]';
51
- }
52
-
53
- const { cookie } = options || {};
54
- const hasOwnPropertyHttpOnly = cookie && Object.prototype.hasOwnProperty.call(
55
- cookie,
56
- 'httpOnly'
57
- );
53
+ const cookie = options?.cookie || {};
58
54
 
59
55
  // httpOnly is true by default if it's not provided
60
- const checkForHTTPOnly =
61
- cookie && hasOwnPropertyHttpOnly
62
- ? !(cookie.httpOnly === true)
63
- : false;
64
-
56
+ const checkForHTTPOnly = 'httpOnly' in cookie ? cookie.httpOnly !== true : false;
65
57
  // secure is false by default
66
- const checkForSecure = cookie ? !(cookie.secure === true) : true;
58
+ if (!checkForHTTPOnly && cookie?.secure) return;
67
59
 
68
- // skip instrumentation since the options are set correctly
69
- if (!checkForHTTPOnly && !checkForSecure) return;
60
+ const displayOptions = {
61
+ cookie: {
62
+ httpOnly: cookie?.httpOnly,
63
+ secure: cookie?.secure
64
+ }
65
+ };
66
+ const optionsString = inspect(displayOptions);
70
67
 
71
68
  const sessionEvent = createSessionEvent({
72
69
  args: [{
73
70
  tracked: false,
74
- value: inspect(options),
71
+ value: optionsString,
75
72
  }],
76
- context: `expressSession(${inspect(data.args)})`,
73
+ context: `expressSession(${optionsString})`,
77
74
  history: [],
78
75
  name: 'express.hookedSessionConstructor',
79
76
  moduleName: 'express-session',
@@ -91,7 +88,7 @@ module.exports = function (core) {
91
88
  constructorOpt: data.hooked,
92
89
  },
93
90
  framework: 'express',
94
- options,
91
+ options: displayOptions,
95
92
  });
96
93
 
97
94
  patcher.patch(data, 'result', {
@@ -100,7 +97,7 @@ module.exports = function (core) {
100
97
  pre(data) {
101
98
  const [, res] = data.args;
102
99
 
103
- const sourceContext = sources.getStore()?.assess;
100
+ const sourceContext = getSourceContext();
104
101
  if (!sourceContext) return;
105
102
 
106
103
  patcher.patch(res, 'setHeader', {
@@ -113,7 +110,7 @@ module.exports = function (core) {
113
110
  handleHttpOnly(sourceContext, value, sessionEvent);
114
111
  }
115
112
 
116
- if (checkForSecure) {
113
+ if (!cookie?.secure) {
117
114
  handleSecure(sourceContext, value, sessionEvent);
118
115
  }
119
116
  }