-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathsecurityHeaders.js
More file actions
96 lines (77 loc) · 2.44 KB
/
securityHeaders.js
File metadata and controls
96 lines (77 loc) · 2.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
'use strict';
const crypto = require('crypto');
function generateNonce() {
return crypto.randomBytes(32).toString('base64');
}
function normalizeOrigin(value) {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
if (!trimmed) return null;
if (trimmed === "'self'" || trimmed === 'self') {
return "'self'";
}
try {
const parsed = new URL(trimmed);
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
return parsed.origin;
}
} catch {
// ignore
}
return null;
}
function isHttpsOrigin(value) {
if (typeof value !== 'string') return false;
try {
return new URL(value.trim()).protocol === 'https:';
} catch {
return false;
}
}
function securityHeaders({ frontendUrl } = {}) {
const frontendOrigin = normalizeOrigin(frontendUrl) || "'self'";
const frameAncestors = frontendOrigin === "'self'" ? "'self'" : frontendOrigin;
const enableHttpsHeaders = isHttpsOrigin(frontendUrl);
return (_req, res, next) => {
if (res.headersSent) return next();
const nonce = generateNonce();
res.locals = res.locals || {};
res.locals.nonce = nonce;
const connectSrc = ["'self'"];
if (frontendOrigin !== "'self'") connectSrc.push(frontendOrigin);
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' https://cdn.socket.io`,
`script-src-elem 'self' 'nonce-${nonce}' https://cdn.socket.io`,
`style-src 'self' 'nonce-${nonce}'`,
"img-src 'self' data: blob:",
"font-src 'self' data:",
`connect-src ${connectSrc.join(' ')}`,
`frame-ancestors ${frameAncestors}`,
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'",
"manifest-src 'self'",
];
if (enableHttpsHeaders) {
csp.push('upgrade-insecure-requests');
}
res.setHeader('Content-Security-Policy', csp.join('; '));
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader(
'Permissions-Policy',
'geolocation=(), microphone=(), camera=(), fullscreen=(self), payment=()'
);
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
res.setHeader('X-DNS-Prefetch-Control', 'off');
if (enableHttpsHeaders) {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
}
next();
};
}
module.exports = securityHeaders;