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