@middy/http-security-headers 3.0.0-alpha.2 → 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 (4) hide show
  1. package/README.md +19 -11
  2. package/index.d.ts +8 -12
  3. package/index.js +240 -105
  4. package/package.json +6 -5
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
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,145 +1,280 @@
1
- import { normalizeHttpResponse } from '@middy/util';
1
+ import { normalizeHttpResponse } from '@middy/util'
2
+
3
+ // Code and Defaults heavily based off https://helmetjs.github.io/
4
+
2
5
  const defaults = {
3
- dnsPrefetchControl: {
4
- 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': ''
5
38
  },
6
- expectCT: {
7
- enforce: true,
8
- maxAge: 30,
9
- reportUri: ''
39
+ contentTypeOptions: {
40
+ action: 'nosniff'
10
41
  },
11
- frameguard: {
12
- action: 'deny'
42
+ crossOriginEmbedderPolicy: {
43
+ policy: 'require-corp'
13
44
  },
14
- hidePoweredBy: {
15
- setTo: null
45
+ crossOriginOpenerPolicy: {
46
+ policy: 'same-origin'
16
47
  },
17
- hsts: {
18
- maxAge: 180 * 24 * 60 * 60,
19
- includeSubDomains: true,
20
- preload: true
48
+ crossOriginResourcePolicy: {
49
+ policy: 'same-origin'
50
+ },
51
+ dnsPrefetchControl: {
52
+ allow: false
21
53
  },
22
- ieNoOpen: {
54
+ downloadOptions: {
23
55
  action: 'noopen'
24
56
  },
25
- noSniff: {
26
- 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': ''
27
106
  },
28
107
  permittedCrossDomainPolicies: {
29
- policy: 'none'
108
+ policy: 'none' // none, master-only, by-content-type, by-ftp-filename, all
109
+ },
110
+ poweredBy: {
111
+ server: ''
30
112
  },
31
113
  referrerPolicy: {
32
114
  policy: 'no-referrer'
33
115
  },
34
- xssFilter: {
35
- 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'
36
131
  }
37
- };
38
- const helmet = {};
39
- const helmetHtmlOnly = {};
132
+ }
40
133
 
41
- helmet.dnsPrefetchControl = (headers, config) => {
42
- headers['X-DNS-Prefetch-Control'] = config.allow ? 'on' : 'off';
43
- return headers;
44
- };
45
-
46
- helmetHtmlOnly.frameguard = (headers, config) => {
47
- headers['X-Frame-Options'] = config.action.toUpperCase();
48
- return headers;
49
- };
50
-
51
- helmet.hidePoweredBy = (headers, config) => {
52
- if (config.setTo) {
53
- headers['X-Powered-By'] = config.setTo;
54
- } else {
55
- delete headers.Server;
56
- delete headers['X-Powered-By'];
134
+ const helmet = {}
135
+ const helmetHtmlOnly = {}
136
+
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'
57
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
+ }
58
162
 
59
- return headers;
60
- };
163
+ // expectCt - DEPRECATED
164
+ // hpkp - DEPRECATED
61
165
 
62
- helmet.hsts = (headers, config) => {
63
- let header = 'max-age=' + Math.round(config.maxAge);
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(', ')
171
+ }
172
+
173
+ helmet.originAgentCluster = (headers, config) => {
174
+ headers['Origin-Agent-Cluster'] = '?1'
175
+ }
176
+
177
+ // https://github.com/helmetjs/referrer-policy
178
+ helmet.referrerPolicy = (headers, config) => {
179
+ headers['Referrer-Policy'] = config.policy
180
+ }
64
181
 
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
+ }
188
+
189
+ // https://github.com/helmetjs/hsts
190
+ helmet.strictTransportSecurity = (headers, config) => {
191
+ let header = 'max-age=' + Math.round(config.maxAge)
65
192
  if (config.includeSubDomains) {
66
- header += '; includeSubDomains';
193
+ header += '; includeSubDomains'
67
194
  }
68
-
69
195
  if (config.preload) {
70
- header += '; preload';
196
+ header += '; preload'
71
197
  }
198
+ headers['Strict-Transport-Security'] = header
199
+ }
72
200
 
73
- headers['Strict-Transport-Security'] = header;
74
- return headers;
75
- };
201
+ // noCache - N/A - separate middleware
76
202
 
77
- helmet.ieNoOpen = (headers, config) => {
78
- headers['X-Download-Options'] = config.action;
79
- return headers;
80
- };
203
+ // X-* //
204
+ // https://github.com/helmetjs/dont-sniff-mimetype
205
+ helmet.contentTypeOptions = (headers, config) => {
206
+ headers['X-Content-Type-Options'] = config.action
207
+ }
81
208
 
82
- helmet.noSniff = (headers, config) => {
83
- headers['X-Content-Type-Options'] = config.action;
84
- return headers;
85
- };
209
+ // https://github.com/helmetjs/dns-Prefetch-control
210
+ helmet.dnsPrefetchControl = (headers, config) => {
211
+ headers['X-DNS-Prefetch-Control'] = config.allow ? 'on' : 'off'
212
+ }
86
213
 
87
- helmet.referrerPolicy = (headers, config) => {
88
- headers['Referrer-Policy'] = config.policy;
89
- return headers;
90
- };
214
+ // https://github.com/helmetjs/ienoopen
215
+ helmet.downloadOptions = (headers, config) => {
216
+ headers['X-Download-Options'] = config.action
217
+ }
91
218
 
92
- helmet.permittedCrossDomainPolicies = (headers, config) => {
93
- headers['X-Permitted-Cross-Domain-Policies'] = config.policy;
94
- return headers;
95
- };
219
+ // https://github.com/helmetjs/frameOptions
220
+ helmetHtmlOnly.frameOptions = (headers, config) => {
221
+ headers['X-Frame-Options'] = config.action.toUpperCase()
222
+ }
96
223
 
97
- helmetHtmlOnly.xssFilter = (headers, config) => {
98
- let header = '1; mode=block';
224
+ // https://github.com/helmetjs/crossdomain
225
+ helmet.permittedCrossDomainPolicies = (headers, config) => {
226
+ headers['X-Permitted-Cross-Domain-Policies'] = config.policy
227
+ }
99
228
 
100
- if (config.reportUri) {
101
- header += '; report=' + config.reportUri;
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']
102
236
  }
237
+ }
103
238
 
104
- headers['X-XSS-Protection'] = header;
105
- return headers;
106
- };
239
+ // https://github.com/helmetjs/x-xss-protection
240
+ helmetHtmlOnly.xssProtection = (headers, config) => {
241
+ let header = '1; mode=block'
242
+ if (config.reportTo) {
243
+ header += '; report=' + config.reportTo
244
+ }
245
+ headers['X-XSS-Protection'] = header
246
+ }
107
247
 
108
248
  const httpSecurityHeadersMiddleware = (opts = {}) => {
109
- const options = { ...defaults,
110
- ...opts
111
- };
112
-
113
- const httpSecurityHeadersMiddlewareAfter = async request => {
114
- var _request$response$hea;
115
-
116
- normalizeHttpResponse(request);
117
- Object.keys(helmet).forEach(key => {
118
- const config = { ...defaults[key],
119
- ...options[key]
120
- };
121
- request.response.headers = helmet[key](request.response.headers, config);
122
- });
123
-
124
- if ((_request$response$hea = request.response.headers['Content-Type']) !== null && _request$response$hea !== void 0 && _request$response$hea.includes('text/html')) {
125
- Object.keys(helmetHtmlOnly).forEach(key => {
126
- const config = { ...defaults[key],
127
- ...options[key]
128
- };
129
- request.response.headers = helmetHtmlOnly[key](request.response.headers, config);
130
- });
131
- }
132
- };
249
+ const options = { ...defaults, ...opts }
250
+
251
+ const httpSecurityHeadersMiddlewareAfter = async (request) => {
252
+ normalizeHttpResponse(request)
133
253
 
134
- const httpSecurityHeadersMiddlewareOnError = async request => {
135
- if (request.response === undefined) return;
136
- return httpSecurityHeadersMiddlewareAfter(request);
137
- };
254
+ Object.keys(helmet).forEach((key) => {
255
+ if (!options[key]) return
256
+ const config = { ...defaults[key], ...options[key] }
257
+ helmet[key](request.response.headers, config)
258
+ })
138
259
 
260
+ if (request.response.headers['Content-Type']?.includes('text/html')) {
261
+ Object.keys(helmetHtmlOnly).forEach((key) => {
262
+ if (!options[key]) return
263
+ const config = { ...defaults[key], ...options[key] }
264
+ helmetHtmlOnly[key](
265
+ request.response.headers,
266
+ config
267
+ )
268
+ })
269
+ }
270
+ }
271
+ const httpSecurityHeadersMiddlewareOnError = async (request) => {
272
+ if (request.response === undefined) return
273
+ return httpSecurityHeadersMiddlewareAfter(request)
274
+ }
139
275
  return {
140
276
  after: httpSecurityHeadersMiddlewareAfter,
141
277
  onError: httpSecurityHeadersMiddlewareOnError
142
- };
143
- };
144
-
145
- export default httpSecurityHeadersMiddleware;
278
+ }
279
+ }
280
+ export default httpSecurityHeadersMiddleware
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@middy/http-security-headers",
3
- "version": "3.0.0-alpha.2",
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
5
  "type": "module",
6
6
  "engines": {
@@ -18,7 +18,8 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "test": "npm run test:unit",
21
- "test:unit": "ava"
21
+ "test:unit": "ava",
22
+ "test:benchmark": "node __benchmarks__/index.js"
22
23
  },
23
24
  "license": "MIT",
24
25
  "keywords": [
@@ -49,11 +50,11 @@
49
50
  "url": "https://github.com/middyjs/middy/issues"
50
51
  },
51
52
  "homepage": "https://github.com/middyjs/middy#readme",
52
- "gitHead": "de30419273ecbff08f367f47c7e320ec981cf145",
53
+ "gitHead": "1441158711580313765e6d156046ef0fade0d156",
53
54
  "dependencies": {
54
- "@middy/util": "^3.0.0-alpha.2"
55
+ "@middy/util": "^3.0.0-alpha.3"
55
56
  },
56
57
  "devDependencies": {
57
- "@middy/core": "^3.0.0-alpha.2"
58
+ "@middy/core": "^3.0.0-alpha.3"
58
59
  }
59
60
  }