@contrast/assess 1.1.0 → 1.2.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 (72) hide show
  1. package/lib/dataflow/propagation/index.js +6 -7
  2. package/lib/dataflow/propagation/install/contrast-methods/index.js +1 -0
  3. package/lib/dataflow/propagation/install/contrast-methods/string.js +56 -0
  4. package/lib/dataflow/propagation/install/string/index.js +1 -0
  5. package/lib/dataflow/propagation/install/string/split.js +108 -0
  6. package/lib/dataflow/sources/index.js +0 -1
  7. package/lib/dataflow/sources/install/http.js +9 -6
  8. package/lib/index.js +7 -2
  9. package/lib/response-scanning/handlers/index.js +274 -0
  10. package/lib/response-scanning/handlers/utils.js +394 -0
  11. package/lib/response-scanning/index.js +36 -0
  12. package/lib/response-scanning/install/http.js +119 -0
  13. package/package.json +3 -3
  14. package/coverage/lcov-report/base.css +0 -224
  15. package/coverage/lcov-report/block-navigation.js +0 -87
  16. package/coverage/lcov-report/favicon.png +0 -0
  17. package/coverage/lcov-report/index.html +0 -266
  18. package/coverage/lcov-report/lib/dataflow/common.js.html +0 -154
  19. package/coverage/lcov-report/lib/dataflow/event-factory.js.html +0 -598
  20. package/coverage/lcov-report/lib/dataflow/index.html +0 -191
  21. package/coverage/lcov-report/lib/dataflow/index.js.html +0 -190
  22. package/coverage/lcov-report/lib/dataflow/propagation/common.js.html +0 -145
  23. package/coverage/lcov-report/lib/dataflow/propagation/index.html +0 -131
  24. package/coverage/lcov-report/lib/dataflow/propagation/index.js.html +0 -190
  25. package/coverage/lcov-report/lib/dataflow/propagation/install/contrast-methods/index.html +0 -131
  26. package/coverage/lcov-report/lib/dataflow/propagation/install/contrast-methods/index.js.html +0 -184
  27. package/coverage/lcov-report/lib/dataflow/propagation/install/contrast-methods/plus.js.html +0 -397
  28. package/coverage/lcov-report/lib/dataflow/propagation/install/string/concat.js.html +0 -478
  29. package/coverage/lcov-report/lib/dataflow/propagation/install/string/index.html +0 -176
  30. package/coverage/lcov-report/lib/dataflow/propagation/install/string/index.js.html +0 -202
  31. package/coverage/lcov-report/lib/dataflow/propagation/install/string/replace.js.html +0 -496
  32. package/coverage/lcov-report/lib/dataflow/propagation/install/string/substring.js.html +0 -415
  33. package/coverage/lcov-report/lib/dataflow/propagation/install/string/trim.js.html +0 -403
  34. package/coverage/lcov-report/lib/dataflow/signatures.js.html +0 -5923
  35. package/coverage/lcov-report/lib/dataflow/sinks/common.js.html +0 -145
  36. package/coverage/lcov-report/lib/dataflow/sinks/index.html +0 -131
  37. package/coverage/lcov-report/lib/dataflow/sinks/index.js.html +0 -211
  38. package/coverage/lcov-report/lib/dataflow/sinks/install/fastify/index.html +0 -146
  39. package/coverage/lcov-report/lib/dataflow/sinks/install/fastify/index.js.html +0 -172
  40. package/coverage/lcov-report/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js.html +0 -319
  41. package/coverage/lcov-report/lib/dataflow/sinks/install/fastify/xss.js.html +0 -721
  42. package/coverage/lcov-report/lib/dataflow/sinks/install/http.js.html +0 -340
  43. package/coverage/lcov-report/lib/dataflow/sinks/install/index.html +0 -116
  44. package/coverage/lcov-report/lib/dataflow/sources/common.js.html +0 -145
  45. package/coverage/lcov-report/lib/dataflow/sources/index.html +0 -131
  46. package/coverage/lcov-report/lib/dataflow/sources/index.js.html +0 -226
  47. package/coverage/lcov-report/lib/dataflow/sources/install/fastify.js.html +0 -379
  48. package/coverage/lcov-report/lib/dataflow/sources/install/http.js.html +0 -502
  49. package/coverage/lcov-report/lib/dataflow/sources/install/index.html +0 -146
  50. package/coverage/lcov-report/lib/dataflow/sources/install/qs.js.html +0 -322
  51. package/coverage/lcov-report/lib/dataflow/tag-utils.js.html +0 -418
  52. package/coverage/lcov-report/lib/dataflow/tracker.js.html +0 -466
  53. package/coverage/lcov-report/lib/dataflow/utils/index.html +0 -116
  54. package/coverage/lcov-report/lib/dataflow/utils/is-vulnerable.js.html +0 -424
  55. package/coverage/lcov-report/lib/index.html +0 -116
  56. package/coverage/lcov-report/lib/index.js.html +0 -193
  57. package/coverage/lcov-report/prettify.css +0 -1
  58. package/coverage/lcov-report/prettify.js +0 -2
  59. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  60. package/coverage/lcov-report/sorter.js +0 -196
  61. package/coverage/lcov.info +0 -4856
  62. package/coverage/tmp/coverage-9548-1675168551025-0.json +0 -1
  63. package/coverage/tmp/coverage-9551-1675168550963-0.json +0 -1
  64. package/coverage/tmp/coverage-9552-1675168550969-0.json +0 -1
  65. package/coverage/tmp/coverage-9553-1675168550970-0.json +0 -1
  66. package/coverage/tmp/coverage-9554-1675168550962-0.json +0 -1
  67. package/coverage/tmp/coverage-9555-1675168550965-0.json +0 -1
  68. package/coverage/tmp/coverage-9556-1675168550964-0.json +0 -1
  69. package/coverage/tmp/coverage-9557-1675168550969-0.json +0 -1
  70. package/coverage/tmp/coverage-9558-1675168550964-0.json +0 -1
  71. package/coverage/tmp/coverage-9559-1675168550971-0.json +0 -1
  72. package/lib/dataflow/sources/install/qs.js +0 -88
@@ -20,15 +20,15 @@ const { callChildComponentMethodsSync } = require('@contrast/common');
20
20
  module.exports = function(core) {
21
21
  const propagation = core.assess.dataflow.propagation = {};
22
22
 
23
- require('./install/string')(core);
24
- require('./install/array-prototype-join')(core);
25
- require('./install/pug')(core);
26
23
  require('./install/contrast-methods')(core);
27
- require('./install/validator')(core);
28
- require('./install/url')(core);
29
24
  require('./install/ejs')(core);
30
- require('./install/encode-uri-component')(core);
25
+ require('./install/pug')(core);
26
+ require('./install/string')(core);
27
+ require('./install/url')(core);
28
+ require('./install/validator')(core);
29
+ require('./install/array-prototype-join')(core);
31
30
  require('./install/decode-uri-component')(core);
31
+ require('./install/encode-uri-component')(core);
32
32
  require('./install/escape-html')(core);
33
33
  require('./install/escape')(core);
34
34
  require('./install/handlebars-utils-escape-expression')(core);
@@ -37,7 +37,6 @@ module.exports = function(core) {
37
37
  require('./install/sql-template-strings')(core);
38
38
  require('./install/unescape')(core);
39
39
 
40
-
41
40
  propagation.install = function() {
42
41
  callChildComponentMethodsSync(propagation, 'install');
43
42
  };
@@ -29,6 +29,7 @@ module.exports = function(core) {
29
29
 
30
30
  require('./add')(core);
31
31
  require('./tag')(core);
32
+ require('./string')(core);
32
33
 
33
34
  return contrastMethodsInstrumentation;
34
35
  };
@@ -0,0 +1,56 @@
1
+ /*
2
+ * Copyright: 2022 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 { patchType } = require('../../common');
19
+
20
+ module.exports = function(core) {
21
+ const {
22
+ scopes: { sources, instrumentation },
23
+ patcher,
24
+ assess: {
25
+ dataflow: { tracker }
26
+ }
27
+ } = core;
28
+
29
+ return core.assess.dataflow.propagation.contrastMethodsInstrumentation.string = {
30
+ install() {
31
+ patcher.patch(global.ContrastMethods, 'String', {
32
+ name: 'ContrastMethods.String',
33
+ patchType,
34
+ post(data) {
35
+ if (!data.result || !sources.getStore() || instrumentation.isLocked()) return;
36
+
37
+ const arg = data.args[0];
38
+ let argInfo = tracker.getData(arg);
39
+
40
+ if (data.obj && !argInfo) {
41
+ argInfo = tracker.getData(data.result.toString());
42
+ }
43
+
44
+ if (!arg || !argInfo) {
45
+ return;
46
+ }
47
+
48
+ const { extern } = tracker.track(data.result, argInfo);
49
+ if (extern) {
50
+ data.result = extern;
51
+ }
52
+ }
53
+ });
54
+ }
55
+ };
56
+ };
@@ -33,6 +33,7 @@ module.exports = function(core) {
33
33
  require('./trim')(core);
34
34
  require('./html-methods')(core);
35
35
  require('./format-methods')(core);
36
+ require('./split')(core);
36
37
 
37
38
  return stringInstrumentation;
38
39
  };
@@ -0,0 +1,108 @@
1
+ /*
2
+ * Copyright: 2022 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 { patchType } = require('../../common');
19
+ const { join } = require('@contrast/common');
20
+ const { createSubsetTags } = require('../../../tag-utils');
21
+
22
+
23
+ module.exports = function(core) {
24
+ const {
25
+ scopes: { sources, instrumentation },
26
+ patcher,
27
+ assess: {
28
+ dataflow: { tracker, eventFactory: { createPropagationEvent } }
29
+ }
30
+ } = core;
31
+
32
+ return core.assess.dataflow.propagation.stringInstrumentation.split = {
33
+ install() {
34
+ const name = 'String.prototype.split';
35
+
36
+ patcher.patch(String.prototype, 'split', {
37
+ name,
38
+ patchType,
39
+ post(data) {
40
+ const { name, args, obj, result, hooked, orig } = data;
41
+ if (
42
+ !obj ||
43
+ !result ||
44
+ args.length === 0 ||
45
+ result.length === 0 ||
46
+ !sources.getStore() ||
47
+ typeof obj !== 'string' ||
48
+ instrumentation.isLocked() ||
49
+ (args.length === 1 && args[0] == null)
50
+ ) return;
51
+
52
+ const objInfo = tracker.getData(obj);
53
+ if (!objInfo) return;
54
+
55
+ let idx = 0;
56
+ for (let i = 0; i < result.length; i++) {
57
+ const res = result[i];
58
+ const start = obj.indexOf(res, idx);
59
+ idx += res.length;
60
+ const objSubstr = obj.substring(start, start + res.length);
61
+ const objSubstrInfo = tracker.getData(objSubstr);
62
+ if (objSubstrInfo) {
63
+ const tags = createSubsetTags(objInfo.tags, start, res.length - 1);
64
+ if (!tags) continue;
65
+
66
+ const event = createPropagationEvent({
67
+ name,
68
+ history: [objInfo],
69
+ object: {
70
+ value: obj,
71
+ isTracked: true,
72
+ },
73
+ args: args.map((arg) => {
74
+ const argInfo = tracker.getData(arg);
75
+ return {
76
+ value: argInfo ? argInfo.value : arg.toString(),
77
+ isTracked: !!argInfo
78
+ };
79
+ }),
80
+ tags,
81
+ result: {
82
+ value: join(result),
83
+ isTracked: false
84
+ },
85
+ stacktraceOpts: {
86
+ constructorOpt: hooked,
87
+ prependFrames: [orig]
88
+ },
89
+ source: 'O',
90
+ target: 'R'
91
+ });
92
+
93
+ if (event) {
94
+ const { extern } = tracker.track(res, event);
95
+ if (extern) {
96
+ data.result[i] = extern;
97
+ }
98
+ }
99
+ }
100
+ }
101
+ },
102
+ });
103
+ },
104
+ uninstall() {
105
+ String.prototype.split = patcher.unwrap(String.prototype.split);
106
+ },
107
+ };
108
+ };
@@ -25,7 +25,6 @@ module.exports = function(core) {
25
25
  // installers
26
26
  require('./install/http')(core);
27
27
  require('./install/fastify')(core);
28
- require('./install/qs')(core);
29
28
 
30
29
  sources.install = function install() {
31
30
  callChildComponentMethodsSync(sources, 'install');
@@ -15,6 +15,7 @@
15
15
 
16
16
  'use strict';
17
17
  const { patchType } = require('../common');
18
+ const { toLowerCase } = require('@contrast/common');
18
19
 
19
20
  // This is only just initiating an async storage for the http source
20
21
  // TODO Tracking the user input
@@ -59,13 +60,13 @@ module.exports = function(core) {
59
60
  const key = obj[i];
60
61
  const value = obj[i + 1];
61
62
 
62
- if (key.toLowerCase() === 'content-type') {
63
+ if (toLowerCase(key) === 'content-type') {
63
64
  store.assess.responseData.contentType = value;
64
65
  }
65
66
  }
66
67
  } else if (typeof obj === 'object') {
67
68
  for (const [key, value] of Object.entries(obj)) {
68
- if (key.toLowerCase() === 'content-type') {
69
+ if (toLowerCase(key) === 'content-type') {
69
70
  store.assess.responseData.contentType = value;
70
71
  }
71
72
  }
@@ -79,7 +80,7 @@ module.exports = function(core) {
79
80
  patchType,
80
81
  pre(data) {
81
82
  const [name = '', value] = data.args;
82
- if (name.toLowerCase() === 'content-type' && value) {
83
+ if (toLowerCase(name) === 'content-type' && value) {
83
84
  store.assess.responseData.contentType = value;
84
85
  }
85
86
  }
@@ -99,11 +100,11 @@ module.exports = function(core) {
99
100
  const headers = {};
100
101
 
101
102
  for (let i = 0; i < req.rawHeaders.length; i += 2) {
102
- const header = req.rawHeaders[i].toLowerCase();
103
+ const header = toLowerCase(req.rawHeaders[i]);
103
104
  headers[header] = req.rawHeaders[i + 1];
104
105
  }
105
106
 
106
- const contentType = headers['content-type']?.toLowerCase();
107
+ const contentType = headers['content-type'] && toLowerCase(headers['content-type']);
107
108
  const reqData = {
108
109
  ip: req.socket.remoteAddress,
109
110
  httpVersion: req.httpVersion,
@@ -125,7 +126,9 @@ module.exports = function(core) {
125
126
  logger.error({ err }, 'Error during assess request handling');
126
127
  }
127
128
 
128
- return next();
129
+ setImmediate(() => {
130
+ next.call(data.obj, ...data.args);
131
+ });
129
132
  }
130
133
 
131
134
  function install() {
package/lib/index.js CHANGED
@@ -15,21 +15,26 @@
15
15
 
16
16
  'use strict';
17
17
 
18
+ const { callChildComponentMethodsSync } = require('@contrast/common');
18
19
  const dataflow = require('./dataflow');
20
+ const responseScanning = require('./response-scanning');
19
21
 
20
22
  module.exports = function assess(core) {
23
+ if (!core.config.assess.enable) return;
24
+
21
25
  const assess = core.assess = {};
22
26
 
23
27
  // Does this order matter? Probably not
24
28
  // 1. dataflow
25
29
  dataflow(core);
30
+ responseScanning(core);
26
31
 
27
- // response-scanning
28
32
  // crypto
29
33
  // static (in coordination with rewriter)
30
34
 
31
35
  assess.install = function() {
32
- core.assess.dataflow.install();
36
+ core.rewriter.install('assess');
37
+ callChildComponentMethodsSync(core.assess, 'install');
33
38
  };
34
39
 
35
40
  return assess;
@@ -0,0 +1,274 @@
1
+ /*
2
+ * Copyright: 2022 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const {
19
+ escapeHtml,
20
+ isHtmlContent,
21
+ getElements,
22
+ getAttribute,
23
+ isParseableResponse,
24
+ checkCacheControlValue,
25
+ checkMetaTags,
26
+ getCspHeaders,
27
+ checkCspSources
28
+ } = require('./utils');
29
+ const { toLowerCase, substring, ResponseScanningRule } = require('@contrast/common');
30
+
31
+ module.exports = function(core) {
32
+ const {
33
+ assess: {
34
+ responseScanning,
35
+ responseScanning: {
36
+ reportFindings
37
+ }
38
+ }
39
+ } = core;
40
+
41
+ responseScanning.handleAutoCompleteMissing = function(sourceContext, { responseHeaders, responseBody }) {
42
+ if (!isHtmlContent(responseHeaders)) {
43
+ return;
44
+ }
45
+
46
+ const elements = getElements('form', responseBody);
47
+
48
+ for (let i = 0; i < elements.length; i++) {
49
+ const autocomplete = getAttribute('autocomplete', elements[i]);
50
+ if (autocomplete !== 'off') {
51
+ reportFindings(sourceContext,
52
+ {
53
+ ruleId: ResponseScanningRule.AUTOCOMPLETE_MISSING,
54
+ vulnerabilityMetadata: {
55
+ attribute: autocomplete,
56
+ html: escapeHtml(elements[i]),
57
+ start: 0,
58
+ end: elements[i].length
59
+ }
60
+ });
61
+ break;
62
+ }
63
+ }
64
+ };
65
+
66
+ responseScanning.handleCacheControlsMissing = function(sourceContext, { responseHeaders, responseBody }) {
67
+ const instructions = [];
68
+
69
+ // de-dupe; this will be re-emitted for parseableBody handlers anyway
70
+ if (isParseableResponse(responseHeaders) && !responseBody) {
71
+ return;
72
+ }
73
+ const cacheControlHeader = responseHeaders['cache-control'];
74
+
75
+ // save the Pragma Header
76
+ if (responseHeaders['pragma']) {
77
+ instructions.push({
78
+ type: 'Header',
79
+ name: 'pragma',
80
+ value: responseHeaders['pragma']
81
+ });
82
+ }
83
+
84
+ if (cacheControlHeader) {
85
+ const [containsNoCache, containsNoStore] = checkCacheControlValue(
86
+ cacheControlHeader
87
+ );
88
+
89
+ // got everything from header so we don't care about meta tags
90
+ if (containsNoCache && containsNoStore) {
91
+ return;
92
+ } else {
93
+ // save the instructions in case meta tags are missing
94
+ instructions.push({
95
+ type: 'Header',
96
+ name: 'cache-control',
97
+ value: cacheControlHeader
98
+ });
99
+ }
100
+ }
101
+
102
+ // we checked the headers now time to check the HTML content
103
+ checkMetaTags({ body: responseBody, cacheControlHeader, instructions });
104
+
105
+ if (instructions.length) {
106
+ reportFindings(sourceContext, {
107
+ ruleId: ResponseScanningRule.CACHE_CONTROLS_MISSING,
108
+ vulnerabilityMetadata: {
109
+ data: JSON.stringify(instructions)
110
+ }
111
+ });
112
+ }
113
+ };
114
+
115
+ responseScanning.handleClickJackingControlsMissing = function(sourceContext, { responseHeaders }) {
116
+ // look for x-frame-options headers with deny or sameorigin
117
+ const xFrameHeaders = responseHeaders['x-frame-options'];
118
+ let hasFrameBusting = false;
119
+
120
+ if (xFrameHeaders) {
121
+ const xFrameHeadersLC = toLowerCase(xFrameHeaders);
122
+ hasFrameBusting =
123
+ xFrameHeadersLC.indexOf('deny') > -1 ||
124
+ xFrameHeadersLC.indexOf('sameorigin') > -1;
125
+ }
126
+
127
+ if (!hasFrameBusting) {
128
+ reportFindings(sourceContext, { ruleId: ResponseScanningRule.CLICKJACKING_CONTROL_MISSING });
129
+ }
130
+ };
131
+
132
+ responseScanning.handleParameterPollution = function(sourceContext, { responseBody }) {
133
+ // look for form tag with missing action attribute.
134
+ // ex: <form method="post">..
135
+ const elements = getElements('form', responseBody);
136
+
137
+ for (let i = 0; i < elements.length; i++) {
138
+ const action = getAttribute('action', elements[i]);
139
+ if (!action) {
140
+ reportFindings(sourceContext, {
141
+ ruleId: ResponseScanningRule.PARAMETER_POLLUTION,
142
+ vulnerabilityMetadata: {
143
+ attribute: action,
144
+ html: escapeHtml(elements[i])
145
+ }
146
+ });
147
+ }
148
+ }
149
+ };
150
+
151
+ /**
152
+ * Checks the response headers for the CSP. If found, and insecure, will return
153
+ * the evidence for reporting.
154
+ *
155
+ * @param {Object} responseHeaders - HTTP headers object.
156
+ * @returns {Object} - Evidence for insecure CSP header.
157
+ */
158
+ responseScanning.handleCspHeader = function(sourceContext, { responseHeaders }) {
159
+ const cspHeaders = getCspHeaders(responseHeaders);
160
+
161
+ // Don't report if not set; this report belongs to 'csp-header-missing'
162
+ if (!cspHeaders) {
163
+ reportFindings(sourceContext, { ruleId: ResponseScanningRule.CSP_HEADER_MISSING });
164
+ return;
165
+ }
166
+
167
+ const vulnerabilityMetadata = checkCspSources(cspHeaders);
168
+
169
+ if (vulnerabilityMetadata.insecure) {
170
+ // We do this because TS API will expect these keys (although it is a typo)
171
+ // When they fix the API we can remove this
172
+ vulnerabilityMetadata.refererSecure = vulnerabilityMetadata.referrerSecure;
173
+ vulnerabilityMetadata.refererValue = vulnerabilityMetadata.referrerValue;
174
+
175
+ delete vulnerabilityMetadata.insecure;
176
+ delete vulnerabilityMetadata.referrerSecure;
177
+ delete vulnerabilityMetadata.referrerValue;
178
+
179
+ reportFindings(sourceContext, { ruleId: ResponseScanningRule.CSP_HEADER_INSECURE, vulnerabilityMetadata });
180
+ }
181
+ };
182
+
183
+ responseScanning.handleHstsHeaderMissing = function(sourceContext, { responseHeaders }) {
184
+ let header = responseHeaders['strict-transport-security'];
185
+ let maxAge;
186
+
187
+ if (header) {
188
+ header = toLowerCase(header);
189
+ const flag = header.indexOf('max-age');
190
+ if (flag > -1) {
191
+ const equal = header.indexOf('=', flag);
192
+ if (equal > -1) {
193
+ const semicolon = header.indexOf(';', equal);
194
+ if (semicolon > -1) {
195
+ maxAge = substring(header, equal + 1, semicolon);
196
+ } else {
197
+ maxAge = substring(header, equal + 1);
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ if (!(parseInt(maxAge) > 0)) {
204
+ reportFindings(sourceContext, {
205
+ ruleId: ResponseScanningRule.HSTS_HEADER_MISSING,
206
+ vulnerabilityMetadata: {
207
+ data: maxAge || ''
208
+ }
209
+ });
210
+ }
211
+ };
212
+
213
+ responseScanning.handlePoweredByHeader = function(sourceContext, { responseHeaders }) {
214
+ const headerName = 'x-powered-by';
215
+ let header = responseHeaders[headerName];
216
+
217
+ if (header) {
218
+ header = toLowerCase(header);
219
+
220
+ const instructions = [
221
+ {
222
+ type: 'Header',
223
+ name: headerName,
224
+ value: header
225
+ }
226
+ ];
227
+
228
+ reportFindings(sourceContext, {
229
+ ruleId: ResponseScanningRule.POWERED_BY_HEADER,
230
+ vulnerabilityMetadata: {
231
+ data: JSON.stringify(instructions)
232
+ }
233
+ });
234
+ }
235
+ };
236
+
237
+ responseScanning.handleXContentTypeHeaderMissing = function(sourceContext, { responseHeaders }) {
238
+ const headerName = 'x-content-type-options';
239
+ let header = responseHeaders[headerName];
240
+
241
+ if (header) {
242
+ header = toLowerCase(header);
243
+ if (header === 'nosniff') {
244
+ return;
245
+ }
246
+ }
247
+
248
+ reportFindings(sourceContext, {
249
+ ruleId: ResponseScanningRule.XCONTENTTYPE_HEADER_MISSING,
250
+ vulnerabilityMetadata: {
251
+ data: header || ''
252
+ }
253
+ });
254
+ };
255
+
256
+ responseScanning.handleXxsProtectionHeaderDisabled = function(sourceContext, { responseHeaders }) {
257
+ const header = responseHeaders['x-xss-protection'];
258
+
259
+ // This header is set by default, so `header` should always be present.
260
+ if (header && header.startsWith('1')) {
261
+ return;
262
+ }
263
+
264
+ reportFindings(sourceContext, {
265
+ ruleId: ResponseScanningRule.XXSPROTECTION_HEADER_DISABLED,
266
+ vulnerabilityMetadata: {
267
+ data: header
268
+ }
269
+ });
270
+ };
271
+
272
+ return responseScanning;
273
+ };
274
+