@middy/http-security-headers 3.0.0-alpha.1 → 3.0.0-alpha.5

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 +149 -51
  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,65 +1,154 @@
1
1
  import { normalizeHttpResponse } from '@middy/util';
2
2
  const defaults = {
3
- dnsPrefetchControl: {
4
- allow: false
3
+ contentSecurityPolicy: {
4
+ 'default-src': "'none'",
5
+ 'base-uri': "'none'",
6
+ sandbox: '',
7
+ 'form-action': "'none'",
8
+ 'frame-ancestors': "'none'",
9
+ 'navigate-to': "'none'",
10
+ 'report-to': 'csp',
11
+ 'require-trusted-types-for': "'script'",
12
+ 'trusted-types': "'none'",
13
+ 'upgrade-insecure-requests': ''
5
14
  },
6
- expectCT: {
7
- enforce: true,
8
- maxAge: 30,
9
- reportUri: ''
15
+ contentTypeOptions: {
16
+ action: 'nosniff'
10
17
  },
11
- frameguard: {
12
- action: 'deny'
18
+ crossOriginEmbedderPolicy: {
19
+ policy: 'require-corp'
13
20
  },
14
- hidePoweredBy: {
15
- setTo: null
21
+ crossOriginOpenerPolicy: {
22
+ policy: 'same-origin'
16
23
  },
17
- hsts: {
18
- maxAge: 180 * 24 * 60 * 60,
19
- includeSubDomains: true,
20
- preload: true
24
+ crossOriginResourcePolicy: {
25
+ policy: 'same-origin'
21
26
  },
22
- ieNoOpen: {
27
+ dnsPrefetchControl: {
28
+ allow: false
29
+ },
30
+ downloadOptions: {
23
31
  action: 'noopen'
24
32
  },
25
- noSniff: {
26
- action: 'nosniff'
33
+ frameOptions: {
34
+ action: 'deny'
35
+ },
36
+ originAgentCluster: {},
37
+ permissionsPolicy: {
38
+ accelerometer: '',
39
+ 'ambient-light-sensor': '',
40
+ autoplay: '',
41
+ battery: '',
42
+ camera: '',
43
+ 'cross-origin-isolated': '',
44
+ 'display-capture': '',
45
+ 'document-domain': '',
46
+ 'encrypted-media': '',
47
+ 'execution-while-not-rendered': '',
48
+ 'execution-while-out-of-viewport': '',
49
+ fullscreen: '',
50
+ geolocation: '',
51
+ gyroscope: '',
52
+ 'keyboard-map': '',
53
+ magnetometer: '',
54
+ microphone: '',
55
+ midi: '',
56
+ 'navigation-override': '',
57
+ payment: '',
58
+ 'picture-in-picture': '',
59
+ 'publickey-credentials-get': '',
60
+ 'screen-wake-lock': '',
61
+ 'sync-xhr': '',
62
+ usb: '',
63
+ 'web-share': '',
64
+ 'xr-spatial-tracking': '',
65
+ 'clipboard-read': '',
66
+ 'clipboard-write': '',
67
+ gamepad: '',
68
+ 'speaker-selection': '',
69
+ 'conversion-measurement': '',
70
+ 'focus-without-user-activation': '',
71
+ hid: '',
72
+ 'idle-detection': '',
73
+ 'interest-cohort': '',
74
+ serial: '',
75
+ 'sync-script': '',
76
+ 'trust-token-redemption': '',
77
+ 'window-placement': '',
78
+ 'vertical-scroll': ''
27
79
  },
28
80
  permittedCrossDomainPolicies: {
29
81
  policy: 'none'
30
82
  },
83
+ poweredBy: {
84
+ server: ''
85
+ },
31
86
  referrerPolicy: {
32
87
  policy: 'no-referrer'
33
88
  },
34
- xssFilter: {
35
- reportUri: ''
89
+ reportTo: {
90
+ maxAge: 365 * 24 * 60 * 60,
91
+ default: '',
92
+ includeSubdomains: true,
93
+ csp: '',
94
+ staple: '',
95
+ xss: ''
96
+ },
97
+ strictTransportSecurity: {
98
+ maxAge: 180 * 24 * 60 * 60,
99
+ includeSubDomains: true,
100
+ preload: true
101
+ },
102
+ xssProtection: {
103
+ reportTo: 'xss'
36
104
  }
37
105
  };
38
106
  const helmet = {};
39
107
  const helmetHtmlOnly = {};
40
108
 
41
- helmet.dnsPrefetchControl = (headers, config) => {
42
- headers['X-DNS-Prefetch-Control'] = config.allow ? 'on' : 'off';
43
- return headers;
109
+ helmetHtmlOnly.contentSecurityPolicy = (headers, config) => {
110
+ let header = Object.keys(config).map(policy => config[policy] ? `${policy} ${config[policy]}` : '').filter(str => str).join('; ');
111
+
112
+ if (config.sandbox === '') {
113
+ header += '; sandbox';
114
+ }
115
+
116
+ if (config['upgrade-insecure-requests'] === '') {
117
+ header += '; upgrade-insecure-requests';
118
+ }
119
+
120
+ headers['Content-Security-Policy'] = header;
44
121
  };
45
122
 
46
- helmetHtmlOnly.frameguard = (headers, config) => {
47
- headers['X-Frame-Options'] = config.action.toUpperCase();
48
- return headers;
123
+ helmetHtmlOnly.crossOriginEmbedderPolicy = (headers, config) => {
124
+ headers['Cross-Origin-Embedder-Policy'] = config.policy;
49
125
  };
50
126
 
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'];
57
- }
127
+ helmetHtmlOnly.crossOriginOpenerPolicy = (headers, config) => {
128
+ headers['Cross-Origin-Opener-Policy'] = config.policy;
129
+ };
130
+
131
+ helmetHtmlOnly.crossOriginResourcePolicy = (headers, config) => {
132
+ headers['Cross-Origin-Resource-Policy'] = config.policy;
133
+ };
58
134
 
59
- return headers;
135
+ helmetHtmlOnly.permissionsPolicy = (headers, config) => {
136
+ headers['Permissions-Policy'] = Object.keys(config).map(policy => `${policy}=${policy === '*' ? '*' : `(${config[policy]})`}`).join(', ');
60
137
  };
61
138
 
62
- helmet.hsts = (headers, config) => {
139
+ helmet.originAgentCluster = (headers, config) => {
140
+ headers['Origin-Agent-Cluster'] = '?1';
141
+ };
142
+
143
+ helmet.referrerPolicy = (headers, config) => {
144
+ headers['Referrer-Policy'] = config.policy;
145
+ };
146
+
147
+ helmetHtmlOnly.reportTo = (headers, config) => {
148
+ headers['Report-To'] = Object.keys(config).map(group => config[group] && group !== 'includeSubdomains' ? `{ "group": "default", "max_age": ${config.maxAge}, "endpoints": [ { "url": "${config[group]}" } ]${group === 'default' ? `, "include_subdomains": ${config.includeSubdomains}` : ''} }` : '').filter(str => str).join(', ');
149
+ };
150
+
151
+ helmet.strictTransportSecurity = (headers, config) => {
63
152
  let header = 'max-age=' + Math.round(config.maxAge);
64
153
 
65
154
  if (config.includeSubDomains) {
@@ -71,38 +160,45 @@ helmet.hsts = (headers, config) => {
71
160
  }
72
161
 
73
162
  headers['Strict-Transport-Security'] = header;
74
- return headers;
75
163
  };
76
164
 
77
- helmet.ieNoOpen = (headers, config) => {
78
- headers['X-Download-Options'] = config.action;
79
- return headers;
165
+ helmet.contentTypeOptions = (headers, config) => {
166
+ headers['X-Content-Type-Options'] = config.action;
80
167
  };
81
168
 
82
- helmet.noSniff = (headers, config) => {
83
- headers['X-Content-Type-Options'] = config.action;
84
- return headers;
169
+ helmet.dnsPrefetchControl = (headers, config) => {
170
+ headers['X-DNS-Prefetch-Control'] = config.allow ? 'on' : 'off';
85
171
  };
86
172
 
87
- helmet.referrerPolicy = (headers, config) => {
88
- headers['Referrer-Policy'] = config.policy;
89
- return headers;
173
+ helmet.downloadOptions = (headers, config) => {
174
+ headers['X-Download-Options'] = config.action;
175
+ };
176
+
177
+ helmetHtmlOnly.frameOptions = (headers, config) => {
178
+ headers['X-Frame-Options'] = config.action.toUpperCase();
90
179
  };
91
180
 
92
181
  helmet.permittedCrossDomainPolicies = (headers, config) => {
93
182
  headers['X-Permitted-Cross-Domain-Policies'] = config.policy;
94
- return headers;
95
183
  };
96
184
 
97
- helmetHtmlOnly.xssFilter = (headers, config) => {
185
+ helmet.poweredBy = (headers, config) => {
186
+ if (config.server) {
187
+ headers['X-Powered-By'] = config.server;
188
+ } else {
189
+ delete headers.Server;
190
+ delete headers['X-Powered-By'];
191
+ }
192
+ };
193
+
194
+ helmetHtmlOnly.xssProtection = (headers, config) => {
98
195
  let header = '1; mode=block';
99
196
 
100
- if (config.reportUri) {
101
- header += '; report=' + config.reportUri;
197
+ if (config.reportTo) {
198
+ header += '; report=' + config.reportTo;
102
199
  }
103
200
 
104
201
  headers['X-XSS-Protection'] = header;
105
- return headers;
106
202
  };
107
203
 
108
204
  const httpSecurityHeadersMiddleware = (opts = {}) => {
@@ -115,18 +211,20 @@ const httpSecurityHeadersMiddleware = (opts = {}) => {
115
211
 
116
212
  normalizeHttpResponse(request);
117
213
  Object.keys(helmet).forEach(key => {
214
+ if (!options[key]) return;
118
215
  const config = { ...defaults[key],
119
216
  ...options[key]
120
217
  };
121
- request.response.headers = helmet[key](request.response.headers, config);
218
+ helmet[key](request.response.headers, config);
122
219
  });
123
220
 
124
221
  if ((_request$response$hea = request.response.headers['Content-Type']) !== null && _request$response$hea !== void 0 && _request$response$hea.includes('text/html')) {
125
222
  Object.keys(helmetHtmlOnly).forEach(key => {
223
+ if (!options[key]) return;
126
224
  const config = { ...defaults[key],
127
225
  ...options[key]
128
226
  };
129
- request.response.headers = helmetHtmlOnly[key](request.response.headers, config);
227
+ helmetHtmlOnly[key](request.response.headers, config);
130
228
  });
131
229
  }
132
230
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@middy/http-security-headers",
3
- "version": "3.0.0-alpha.1",
3
+ "version": "3.0.0-alpha.5",
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": "a14125c6b2e21b181824f9985a919a47f1e4711f",
53
+ "gitHead": "cf6a1b02a2e163bea353b10146d67e0d95ef8072",
53
54
  "dependencies": {
54
- "@middy/util": "^3.0.0-alpha.1"
55
+ "@middy/util": "^3.0.0-alpha.5"
55
56
  },
56
57
  "devDependencies": {
57
- "@middy/core": "^3.0.0-alpha.1"
58
+ "@middy/core": "^3.0.0-alpha.5"
58
59
  }
59
60
  }