@middy/http-security-headers 2.5.6 → 3.0.0-alpha.3

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 (5) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +20 -12
  3. package/index.d.ts +8 -12
  4. package/index.js +195 -80
  5. package/package.json +10 -8
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2017-2021 Luciano Mammino, will Farrell and the [Middy team](https://github.com/middyjs/middy/graphs/contributors)
3
+ Copyright (c) 2017-2022 Luciano Mammino, will Farrell and the [Middy team](https://github.com/middyjs/middy/graphs/contributors)
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -36,18 +36,26 @@ To install this middleware you can use NPM:
36
36
  npm install --save @middy/http-security-headers
37
37
  ```
38
38
 
39
-
40
39
  ## Options
41
-
42
- - `dnsPrefetchControl` controls browser DNS prefetching
43
- - `expectCt` for handling Certificate Transparency (Future Feature)
44
- - `frameguard` to prevent clickjacking
45
- - `hidePoweredBy` to remove the Server/X-Powered-By header
46
- - `hsts` for HTTP Strict Transport Security
47
- - `ieNoOpen` sets X-Download-Options for IE8+
48
- - `noSniff` to keep clients from sniffing the MIME type
49
- - `referrerPolicy` to hide the Referer header
50
- - `xssFilter` adds some small XSS protections
40
+ Setting an option to `false` to cause that rule to be ignored.
41
+
42
+ ### All Responses
43
+ - `originAgentCluster`: Default to `{}` to include
44
+ - `referrerPolicy`: Default to `{ policy: 'no-referrer' }`
45
+ - `strictTransportSecurity`: Default to `{ maxAge: 15552000, includeSubDomains: true, preload: true }`
46
+ - X-`dnsPrefetchControl`: Default to `{ allow: false }`
47
+ - X-`downloadOptions`: Default to `{ action: 'noopen' }`
48
+ - X-`poweredBy`: Default to `{ server: '' }` to remove `Server` and `X-Powered-By`
49
+ - X-`contentTypeOptions`: Default to `{ action: 'nosniff' }`
50
+ ### HTML Responses
51
+ - `contentSecurityPolicy`: Default to `{ 'default-src': "'none'", 'base-uri':"'none'", 'sandbox':'', 'form-action':"'none'", 'frame-ancestors':"'none'", 'navigate-to':"'none'", 'report-to':'csp', 'require-trusted-types-for':"'script'", 'trusted-types':"'none'", 'upgrade-insecure-requests':'' }`
52
+ - `crossOriginEmbedderPolicy`: Default to `{ policy: 'require-corp' }`
53
+ - `crossOriginOpenerPolicy`: Default to `{ policy: 'same-origin' }`
54
+ - `crossOriginResourcePolicy`: Default to `{ policy: 'same-origin' }`
55
+ - `permissionsPolicy`: Default to `{ *:'', ... }` where all allowed values are set to disable
56
+ - `reportTo`: Defaults to `{ maxAge: 31536000, default: '', includeSubdomains: true, csp: '', staple:'', xss: '' }` which won't report by default, needs setting
57
+ - X-`frameOptions`: Default to `{ action: 'deny' }`
58
+ - X-`xssProtection`: Defaults to `{ reportUri: '' }'`
51
59
 
52
60
 
53
61
  ## Sample usage
@@ -77,7 +85,7 @@ Everyone is very welcome to contribute to this repository. Feel free to [raise i
77
85
 
78
86
  ## License
79
87
 
80
- Licensed under [MIT License](LICENSE). Copyright (c) 2017-2021 Luciano Mammino, will Farrell, and the [Middy team](https://github.com/middyjs/middy/graphs/contributors).
88
+ Licensed under [MIT License](LICENSE). Copyright (c) 2017-2022 Luciano Mammino, will Farrell, and the [Middy team](https://github.com/middyjs/middy/graphs/contributors).
81
89
 
82
90
  <a href="https://app.fossa.io/projects/git%2Bgithub.com%2Fmiddyjs%2Fmiddy?ref=badge_large">
83
91
  <img src="https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmiddyjs%2Fmiddy.svg?type=large" alt="FOSSA Status" style="max-width:100%;">
package/index.d.ts CHANGED
@@ -4,32 +4,28 @@ interface Options {
4
4
  dnsPrefetchControl?: {
5
5
  allow?: boolean
6
6
  }
7
- expectCT?: {
8
- enforce?: boolean
9
- maxAge?: number
10
- reportUri?: string
11
- }
12
- frameguard?: {
7
+ frameOptions?: {
13
8
  action?: string
14
9
  }
15
- hidePoweredBy?: {
16
- setTo: string
10
+ poweredBy?: {
11
+ server: string
17
12
  }
18
- hsts?: {
13
+ strictTransportSecurity?: {
19
14
  maxAge?: number
20
15
  includeSubDomains?: boolean
21
16
  preload?: boolean
22
17
  }
23
- ieNoOpen?: {
18
+ downloadOptions?: {
24
19
  action?: string
25
20
  }
26
- noSniff?: {
21
+ contentTypeOptions?: {
27
22
  action?: string
28
23
  }
24
+ originAgentCluster?: boolean
29
25
  referrerPolicy?: {
30
26
  policy?: string
31
27
  }
32
- xssFilter?: {
28
+ xssProtection?: {
33
29
  reportUri?: string
34
30
  }
35
31
  }
package/index.js CHANGED
@@ -1,91 +1,193 @@
1
- const { normalizeHttpResponse } = require('@middy/util')
1
+ import { normalizeHttpResponse } from '@middy/util'
2
2
 
3
3
  // Code and Defaults heavily based off https://helmetjs.github.io/
4
4
 
5
5
  const defaults = {
6
- // contentDisposition: {
7
- // filename: undefined
8
- // },
9
- dnsPrefetchControl: {
10
- allow: false
6
+ contentSecurityPolicy: {
7
+ // Fetch directives
8
+ // 'child-src': '', // fallback default-src
9
+ // 'connect-src': '', // fallback default-src
10
+ 'default-src': "'none'",
11
+ // 'font-src':'', // fallback default-src
12
+ // 'frame-src':'', // fallback child-src > default-src
13
+ // 'img-src':'', // fallback default-src
14
+ // 'manifest-src':'', // fallback default-src
15
+ // 'media-src':'', // fallback default-src
16
+ // 'object-src':'', // fallback default-src
17
+ // 'prefetch-src':'', // fallback default-src
18
+ // 'script-src':'', // fallback default-src
19
+ // 'script-src-elem':'', // fallback script-src > default-src
20
+ // 'script-src-attr':'', // fallback script-src > default-src
21
+ // 'style-src':'', // fallback default-src
22
+ // 'style-src-elem':'', // fallback style-src > default-src
23
+ // 'style-src-attr':'', // fallback style-src > default-src
24
+ // 'worker-src':'', // fallback child-src > script-src > default-src
25
+ // Document directives
26
+ 'base-uri': "'none'",
27
+ sandbox: '',
28
+ // Navigation directives
29
+ 'form-action': "'none'",
30
+ 'frame-ancestors': "'none'",
31
+ 'navigate-to': "'none'",
32
+ // Reporting directives
33
+ 'report-to': 'csp',
34
+ // Other directives
35
+ 'require-trusted-types-for': "'script'",
36
+ 'trusted-types': "'none'",
37
+ 'upgrade-insecure-requests': ''
11
38
  },
12
- expectCT: {
13
- enforce: true,
14
- maxAge: 30,
15
- reportUri: ''
39
+ contentTypeOptions: {
40
+ action: 'nosniff'
16
41
  },
17
- frameguard: {
18
- action: 'deny'
42
+ crossOriginEmbedderPolicy: {
43
+ policy: 'require-corp'
19
44
  },
20
- hidePoweredBy: {
21
- setTo: null
45
+ crossOriginOpenerPolicy: {
46
+ policy: 'same-origin'
22
47
  },
23
- hsts: {
24
- maxAge: 180 * 24 * 60 * 60,
25
- includeSubDomains: true,
26
- preload: true
48
+ crossOriginResourcePolicy: {
49
+ policy: 'same-origin'
50
+ },
51
+ dnsPrefetchControl: {
52
+ allow: false
27
53
  },
28
- ieNoOpen: {
54
+ downloadOptions: {
29
55
  action: 'noopen'
30
56
  },
31
- noSniff: {
32
- action: 'nosniff'
57
+ frameOptions: {
58
+ action: 'deny'
59
+ },
60
+ originAgentCluster: {},
61
+ permissionsPolicy: {
62
+ // Standard
63
+ accelerometer: '',
64
+ 'ambient-light-sensor': '',
65
+ autoplay: '',
66
+ battery: '',
67
+ camera: '',
68
+ 'cross-origin-isolated': '',
69
+ 'display-capture': '',
70
+ 'document-domain': '',
71
+ 'encrypted-media': '',
72
+ 'execution-while-not-rendered': '',
73
+ 'execution-while-out-of-viewport': '',
74
+ fullscreen: '',
75
+ geolocation: '',
76
+ gyroscope: '',
77
+ 'keyboard-map': '',
78
+ magnetometer: '',
79
+ microphone: '',
80
+ midi: '',
81
+ 'navigation-override': '',
82
+ payment: '',
83
+ 'picture-in-picture': '',
84
+ 'publickey-credentials-get': '',
85
+ 'screen-wake-lock': '',
86
+ 'sync-xhr': '',
87
+ usb: '',
88
+ 'web-share': '',
89
+ 'xr-spatial-tracking': '',
90
+ // Proposed
91
+ 'clipboard-read': '',
92
+ 'clipboard-write': '',
93
+ gamepad: '',
94
+ 'speaker-selection': '',
95
+ // Experimental
96
+ 'conversion-measurement': '',
97
+ 'focus-without-user-activation': '',
98
+ hid: '',
99
+ 'idle-detection': '',
100
+ 'interest-cohort': '',
101
+ serial: '',
102
+ 'sync-script': '',
103
+ 'trust-token-redemption': '',
104
+ 'window-placement': '',
105
+ 'vertical-scroll': ''
33
106
  },
34
107
  permittedCrossDomainPolicies: {
35
108
  policy: 'none' // none, master-only, by-content-type, by-ftp-filename, all
36
109
  },
110
+ poweredBy: {
111
+ server: ''
112
+ },
37
113
  referrerPolicy: {
38
114
  policy: 'no-referrer'
39
115
  },
40
- xssFilter: {
41
- reportUri: ''
116
+ reportTo: {
117
+ maxAge: 365 * 24 * 60 * 60,
118
+ default: '',
119
+ includeSubdomains: true,
120
+ csp: '',
121
+ staple: '',
122
+ xss: ''
123
+ },
124
+ strictTransportSecurity: {
125
+ maxAge: 180 * 24 * 60 * 60,
126
+ includeSubDomains: true,
127
+ preload: true
128
+ },
129
+ xssProtection: {
130
+ reportTo: 'xss'
42
131
  }
43
132
  }
44
133
 
45
134
  const helmet = {}
46
135
  const helmetHtmlOnly = {}
47
136
 
48
- // OWASP ASVS 14.4.2
49
- // API Gateway strips out this header :(
50
- // helmet.content = (headers, config) => {
51
- // const filename = config.filename ?? `api.${headers?.['Content-Type'].split(/[/;]/)[1] ?? 'json'}`
52
- // headers['Content-Disposition'] = `attachment; filename="${filename}"`
53
- // }
54
-
55
- // contentSecurityPolicy - N/A - no HTML
56
- // featurePolicy - N/A - no HTML
137
+ // *** https://github.com/helmetjs/helmet/tree/main/middlewares *** //
138
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
139
+ helmetHtmlOnly.contentSecurityPolicy = (headers, config) => {
140
+ let header = Object.keys(config)
141
+ .map(policy => config[policy] ? `${policy} ${config[policy]}` : '')
142
+ .filter(str => str)
143
+ .join('; ')
144
+ if (config.sandbox === '') {
145
+ header += '; sandbox'
146
+ }
147
+ if (config['upgrade-insecure-requests'] === '') {
148
+ header += '; upgrade-insecure-requests'
149
+ }
150
+ headers['Content-Security-Policy'] = header
151
+ }
152
+ // crossdomain - N/A - for Adobe products
153
+ helmetHtmlOnly.crossOriginEmbedderPolicy = (headers, config) => {
154
+ headers['Cross-Origin-Embedder-Policy'] = config.policy
155
+ }
156
+ helmetHtmlOnly.crossOriginOpenerPolicy = (headers, config) => {
157
+ headers['Cross-Origin-Opener-Policy'] = config.policy
158
+ }
159
+ helmetHtmlOnly.crossOriginResourcePolicy = (headers, config) => {
160
+ headers['Cross-Origin-Resource-Policy'] = config.policy
161
+ }
57
162
 
58
- // crossdomain - N/A - For Adobe products
163
+ // expectCt - DEPRECATED
164
+ // hpkp - DEPRECATED
59
165
 
60
- // https://github.com/helmetjs/dns-Prefetch-control
61
- helmet.dnsPrefetchControl = (headers, config) => {
62
- headers['X-DNS-Prefetch-Control'] = config.allow ? 'on' : 'off'
63
- return headers
166
+ // https://www.permissionspolicy.com/
167
+ helmetHtmlOnly.permissionsPolicy = (headers, config) => {
168
+ headers['Permissions-Policy'] = Object.keys(config)
169
+ .map(policy => `${policy}=${policy === '*' ? '*' : `(${config[policy]})`}`)
170
+ .join(', ')
64
171
  }
65
172
 
66
- // expectCt - in-progress spec
67
-
68
- // https://github.com/helmetjs/frameguard
69
- helmetHtmlOnly.frameguard = (headers, config) => {
70
- headers['X-Frame-Options'] = config.action.toUpperCase()
71
- return headers
173
+ helmet.originAgentCluster = (headers, config) => {
174
+ headers['Origin-Agent-Cluster'] = '?1'
72
175
  }
73
176
 
74
- // https://github.com/helmetjs/hide-powered-by
75
- helmet.hidePoweredBy = (headers, config) => {
76
- if (config.setTo) {
77
- headers['X-Powered-By'] = config.setTo
78
- } else {
79
- Reflect.deleteProperty(headers, 'Server')
80
- Reflect.deleteProperty(headers, 'X-Powered-By')
81
- }
82
- return headers
177
+ // https://github.com/helmetjs/referrer-policy
178
+ helmet.referrerPolicy = (headers, config) => {
179
+ headers['Referrer-Policy'] = config.policy
83
180
  }
84
181
 
85
- // hpkp - deprecated
182
+ helmetHtmlOnly.reportTo = (headers, config) => {
183
+ headers['Report-To'] = Object.keys(config)
184
+ .map(group => (config[group] && group !== 'includeSubdomains') ? `{ "group": "default", "max_age": ${config.maxAge}, "endpoints": [ { "url": "${config[group]}" } ]${group === 'default' ? `, "include_subdomains": ${config.includeSubdomains}` : ''} }` : '')
185
+ .filter(str => str)
186
+ .join(', ')
187
+ }
86
188
 
87
189
  // https://github.com/helmetjs/hsts
88
- helmet.hsts = (headers, config) => {
190
+ helmet.strictTransportSecurity = (headers, config) => {
89
191
  let header = 'max-age=' + Math.round(config.maxAge)
90
192
  if (config.includeSubDomains) {
91
193
  header += '; includeSubDomains'
@@ -94,72 +196,85 @@ helmet.hsts = (headers, config) => {
94
196
  header += '; preload'
95
197
  }
96
198
  headers['Strict-Transport-Security'] = header
97
- return headers
98
- }
99
-
100
- // https://github.com/helmetjs/ienoopen
101
- helmet.ieNoOpen = (headers, config) => {
102
- headers['X-Download-Options'] = config.action
103
- return headers
104
199
  }
105
200
 
106
201
  // noCache - N/A - separate middleware
107
202
 
203
+ // X-* //
108
204
  // https://github.com/helmetjs/dont-sniff-mimetype
109
- helmet.noSniff = (headers, config) => {
205
+ helmet.contentTypeOptions = (headers, config) => {
110
206
  headers['X-Content-Type-Options'] = config.action
111
- return headers
112
207
  }
113
208
 
114
- // https://github.com/helmetjs/referrer-policy
115
- helmet.referrerPolicy = (headers, config) => {
116
- headers['Referrer-Policy'] = config.policy
117
- return headers
209
+ // https://github.com/helmetjs/dns-Prefetch-control
210
+ helmet.dnsPrefetchControl = (headers, config) => {
211
+ headers['X-DNS-Prefetch-Control'] = config.allow ? 'on' : 'off'
212
+ }
213
+
214
+ // https://github.com/helmetjs/ienoopen
215
+ helmet.downloadOptions = (headers, config) => {
216
+ headers['X-Download-Options'] = config.action
217
+ }
218
+
219
+ // https://github.com/helmetjs/frameOptions
220
+ helmetHtmlOnly.frameOptions = (headers, config) => {
221
+ headers['X-Frame-Options'] = config.action.toUpperCase()
118
222
  }
119
223
 
120
224
  // https://github.com/helmetjs/crossdomain
121
225
  helmet.permittedCrossDomainPolicies = (headers, config) => {
122
226
  headers['X-Permitted-Cross-Domain-Policies'] = config.policy
123
- return headers
227
+ }
228
+
229
+ // https://github.com/helmetjs/hide-powered-by
230
+ helmet.poweredBy = (headers, config) => {
231
+ if (config.server) {
232
+ headers['X-Powered-By'] = config.server
233
+ } else {
234
+ delete headers.Server
235
+ delete headers['X-Powered-By']
236
+ }
124
237
  }
125
238
 
126
239
  // https://github.com/helmetjs/x-xss-protection
127
- helmetHtmlOnly.xssFilter = (headers, config) => {
240
+ helmetHtmlOnly.xssProtection = (headers, config) => {
128
241
  let header = '1; mode=block'
129
- if (config.reportUri) {
130
- header += '; report=' + config.reportUri
242
+ if (config.reportTo) {
243
+ header += '; report=' + config.reportTo
131
244
  }
132
245
  headers['X-XSS-Protection'] = header
133
- return headers
134
246
  }
135
247
 
136
248
  const httpSecurityHeadersMiddleware = (opts = {}) => {
137
249
  const options = { ...defaults, ...opts }
138
250
 
139
251
  const httpSecurityHeadersMiddlewareAfter = async (request) => {
140
- request.response = normalizeHttpResponse(request.response)
252
+ normalizeHttpResponse(request)
141
253
 
142
254
  Object.keys(helmet).forEach((key) => {
255
+ if (!options[key]) return
143
256
  const config = { ...defaults[key], ...options[key] }
144
- request.response.headers = helmet[key](request.response.headers, config)
257
+ helmet[key](request.response.headers, config)
145
258
  })
146
259
 
147
- if (request.response.headers?.['Content-Type']?.includes('text/html')) {
260
+ if (request.response.headers['Content-Type']?.includes('text/html')) {
148
261
  Object.keys(helmetHtmlOnly).forEach((key) => {
262
+ if (!options[key]) return
149
263
  const config = { ...defaults[key], ...options[key] }
150
- request.response.headers = helmetHtmlOnly[key](
264
+ helmetHtmlOnly[key](
151
265
  request.response.headers,
152
266
  config
153
267
  )
154
268
  })
155
269
  }
156
270
  }
157
-
158
- const httpSecurityHeadersMiddlewareOnError = httpSecurityHeadersMiddlewareAfter
159
-
271
+ const httpSecurityHeadersMiddlewareOnError = async (request) => {
272
+ if (request.response === undefined) return
273
+ return httpSecurityHeadersMiddlewareAfter(request)
274
+ }
160
275
  return {
161
276
  after: httpSecurityHeadersMiddlewareAfter,
162
277
  onError: httpSecurityHeadersMiddlewareOnError
163
278
  }
164
279
  }
165
- module.exports = httpSecurityHeadersMiddleware
280
+ export default httpSecurityHeadersMiddleware
package/package.json CHANGED
@@ -1,23 +1,25 @@
1
1
  {
2
2
  "name": "@middy/http-security-headers",
3
- "version": "2.5.6",
3
+ "version": "3.0.0-alpha.3",
4
4
  "description": "Applies best practice security headers to responses. It's a simplified port of HelmetJS",
5
- "type": "commonjs",
5
+ "type": "module",
6
6
  "engines": {
7
- "node": ">=12"
7
+ "node": ">=14"
8
8
  },
9
9
  "engineStrict": true,
10
10
  "publishConfig": {
11
11
  "access": "public"
12
12
  },
13
- "main": "index.js",
13
+ "exports": "./index.js",
14
14
  "types": "index.d.ts",
15
15
  "files": [
16
+ "index.js",
16
17
  "index.d.ts"
17
18
  ],
18
19
  "scripts": {
19
20
  "test": "npm run test:unit",
20
- "test:unit": "ava"
21
+ "test:unit": "ava",
22
+ "test:benchmark": "node __benchmarks__/index.js"
21
23
  },
22
24
  "license": "MIT",
23
25
  "keywords": [
@@ -48,11 +50,11 @@
48
50
  "url": "https://github.com/middyjs/middy/issues"
49
51
  },
50
52
  "homepage": "https://github.com/middyjs/middy#readme",
51
- "gitHead": "0c789f55b4adf691f977b0d9904d1a805bb3bb2b",
53
+ "gitHead": "1441158711580313765e6d156046ef0fade0d156",
52
54
  "dependencies": {
53
- "@middy/util": "^2.5.6"
55
+ "@middy/util": "^3.0.0-alpha.3"
54
56
  },
55
57
  "devDependencies": {
56
- "@middy/core": "^2.5.6"
58
+ "@middy/core": "^3.0.0-alpha.3"
57
59
  }
58
60
  }