1 | import { normalizeHttpResponse } from '@middy/util';
|
2 | const defaults = {
|
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': ''
|
14 | },
|
15 | contentTypeOptions: {
|
16 | action: 'nosniff'
|
17 | },
|
18 | crossOriginEmbedderPolicy: {
|
19 | policy: 'require-corp'
|
20 | },
|
21 | crossOriginOpenerPolicy: {
|
22 | policy: 'same-origin'
|
23 | },
|
24 | crossOriginResourcePolicy: {
|
25 | policy: 'same-origin'
|
26 | },
|
27 | dnsPrefetchControl: {
|
28 | allow: false
|
29 | },
|
30 | downloadOptions: {
|
31 | action: 'noopen'
|
32 | },
|
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': ''
|
79 | },
|
80 | permittedCrossDomainPolicies: {
|
81 | policy: 'none'
|
82 | },
|
83 | poweredBy: {
|
84 | server: ''
|
85 | },
|
86 | referrerPolicy: {
|
87 | policy: 'no-referrer'
|
88 | },
|
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'
|
104 | }
|
105 | };
|
106 | const helmet = {};
|
107 | const helmetHtmlOnly = {};
|
108 | helmetHtmlOnly.contentSecurityPolicy = (headers, config)=>{
|
109 | let header = Object.keys(config).map((policy)=>config[policy] ? `${policy} ${config[policy]}` : '').filter((str)=>str).join('; ');
|
110 | if (config.sandbox === '') {
|
111 | header += '; sandbox';
|
112 | }
|
113 | if (config['upgrade-insecure-requests'] === '') {
|
114 | header += '; upgrade-insecure-requests';
|
115 | }
|
116 | headers['Content-Security-Policy'] = header;
|
117 | };
|
118 | helmetHtmlOnly.crossOriginEmbedderPolicy = (headers, config)=>{
|
119 | headers['Cross-Origin-Embedder-Policy'] = config.policy;
|
120 | };
|
121 | helmetHtmlOnly.crossOriginOpenerPolicy = (headers, config)=>{
|
122 | headers['Cross-Origin-Opener-Policy'] = config.policy;
|
123 | };
|
124 | helmetHtmlOnly.crossOriginResourcePolicy = (headers, config)=>{
|
125 | headers['Cross-Origin-Resource-Policy'] = config.policy;
|
126 | };
|
127 | helmetHtmlOnly.permissionsPolicy = (headers, config)=>{
|
128 | headers['Permissions-Policy'] = Object.keys(config).map((policy)=>`${policy}=${config[policy] === '*' ? '*' : '(' + config[policy] + ')'}`).join(', ');
|
129 | };
|
130 | helmet.originAgentCluster = (headers, config)=>{
|
131 | headers['Origin-Agent-Cluster'] = '?1';
|
132 | };
|
133 | helmet.referrerPolicy = (headers, config)=>{
|
134 | headers['Referrer-Policy'] = config.policy;
|
135 | };
|
136 | helmetHtmlOnly.reportTo = (headers, config)=>{
|
137 | headers['Report-To'] = Object.keys(config).map((group)=>{
|
138 | const includeSubdomains = group === 'default' ? `, "include_subdomains": ${config.includeSubdomains}` : '';
|
139 | return config[group] && group !== 'includeSubdomains' ? `{ "group": "default", "max_age": ${config.maxAge}, "endpoints": [ { "url": "${config[group]}" } ]${includeSubdomains} }` : '';
|
140 | }).filter((str)=>str).join(', ');
|
141 | };
|
142 | helmet.strictTransportSecurity = (headers, config)=>{
|
143 | let header = 'max-age=' + Math.round(config.maxAge);
|
144 | if (config.includeSubDomains) {
|
145 | header += '; includeSubDomains';
|
146 | }
|
147 | if (config.preload) {
|
148 | header += '; preload';
|
149 | }
|
150 | headers['Strict-Transport-Security'] = header;
|
151 | };
|
152 | helmet.contentTypeOptions = (headers, config)=>{
|
153 | headers['X-Content-Type-Options'] = config.action;
|
154 | };
|
155 | helmet.dnsPrefetchControl = (headers, config)=>{
|
156 | headers['X-DNS-Prefetch-Control'] = config.allow ? 'on' : 'off';
|
157 | };
|
158 | helmet.downloadOptions = (headers, config)=>{
|
159 | headers['X-Download-Options'] = config.action;
|
160 | };
|
161 | helmetHtmlOnly.frameOptions = (headers, config)=>{
|
162 | headers['X-Frame-Options'] = config.action.toUpperCase();
|
163 | };
|
164 | helmet.permittedCrossDomainPolicies = (headers, config)=>{
|
165 | headers['X-Permitted-Cross-Domain-Policies'] = config.policy;
|
166 | };
|
167 | helmet.poweredBy = (headers, config)=>{
|
168 | if (config.server) {
|
169 | headers['X-Powered-By'] = config.server;
|
170 | } else {
|
171 | delete headers.Server;
|
172 | delete headers['X-Powered-By'];
|
173 | }
|
174 | };
|
175 | helmetHtmlOnly.xssProtection = (headers, config)=>{
|
176 | let header = '1; mode=block';
|
177 | if (config.reportTo) {
|
178 | header += '; report=' + config.reportTo;
|
179 | }
|
180 | headers['X-XSS-Protection'] = header;
|
181 | };
|
182 | const httpSecurityHeadersMiddleware = (opts = {})=>{
|
183 | const options = {
|
184 | ...defaults,
|
185 | ...opts
|
186 | };
|
187 | const httpSecurityHeadersMiddlewareAfter = async (request)=>{
|
188 | normalizeHttpResponse(request);
|
189 | Object.keys(helmet).forEach((key)=>{
|
190 | if (!options[key]) return;
|
191 | const config = {
|
192 | ...defaults[key],
|
193 | ...options[key]
|
194 | };
|
195 | helmet[key](request.response.headers, config);
|
196 | });
|
197 | if (request.response.headers['Content-Type']?.includes('text/html')) {
|
198 | Object.keys(helmetHtmlOnly).forEach((key)=>{
|
199 | if (!options[key]) return;
|
200 | const config = {
|
201 | ...defaults[key],
|
202 | ...options[key]
|
203 | };
|
204 | helmetHtmlOnly[key](request.response.headers, config);
|
205 | });
|
206 | }
|
207 | };
|
208 | const httpSecurityHeadersMiddlewareOnError = async (request)=>{
|
209 | if (request.response === undefined) return;
|
210 | httpSecurityHeadersMiddlewareAfter(request);
|
211 | };
|
212 | return {
|
213 | after: httpSecurityHeadersMiddlewareAfter,
|
214 | onError: httpSecurityHeadersMiddlewareOnError
|
215 | };
|
216 | };
|
217 | export default httpSecurityHeadersMiddleware;
|
218 |
|