-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathwebSecurity.js
More file actions
115 lines (104 loc) · 4.48 KB
/
Copy pathwebSecurity.js
File metadata and controls
115 lines (104 loc) · 4.48 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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
'use strict';
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
const SENSITIVE_PATH_RE = /^\/(?:api|admin|verify|oauth|ticket-transcript)(?:\/|$)/;
const BLOCKED_STATIC_RE = /(^\/(admin|verify)\.html$)|(\.(bak\d*|orig|old|tmp|save|swp|swo|copy|log)$)|(~$)/i;
const DEFAULT_CSP = Object.freeze([
"default-src 'self'",
"script-src 'self' https://challenges.cloudflare.com",
"style-src 'self' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com data:",
"img-src 'self' data: https:",
"connect-src 'self' https://challenges.cloudflare.com",
'frame-src https://challenges.cloudflare.com',
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
"form-action 'self'",
]);
function parseOrigin(value, name) {
let url;
try { url = new URL(String(value)); }
catch { throw new TypeError(`${name} must contain absolute http(s) origins`); }
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
throw new TypeError(`${name} must contain credential-free http(s) origins`);
}
return url.origin;
}
function buildAllowedOrigins(baseUrl, extraOrigins = []) {
const base = new URL(parseOrigin(baseUrl, 'BASE_URL'));
const rawExtra = Array.isArray(extraOrigins)
? extraOrigins
: String(extraOrigins || '').split(',').map(value => value.trim()).filter(Boolean);
if (rawExtra.length > 32) throw new TypeError('CORS_ORIGINS supports at most 32 origins');
const origins = new Set([base.origin]);
rawExtra.forEach(value => origins.add(parseOrigin(value, 'CORS_ORIGINS')));
if (['localhost', '127.0.0.1', '[::1]'].includes(base.hostname)) {
const suffix = base.port ? `:${base.port}` : '';
origins.add(`${base.protocol}//localhost${suffix}`);
origins.add(`${base.protocol}//127.0.0.1${suffix}`);
origins.add(`${base.protocol}//[::1]${suffix}`);
}
return origins;
}
function createWebSecurity({ baseUrl, extraOrigins = [], csp = DEFAULT_CSP } = {}) {
const allowedOrigins = buildAllowedOrigins(baseUrl, extraOrigins);
const secureBase = new URL(baseUrl).protocol === 'https:';
function isAllowedOrigin(origin) {
if (!origin) return true;
try { return allowedOrigins.has(new URL(origin).origin); }
catch { return false; }
}
function headers(req, res, next) {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Referrer-Policy', 'no-referrer');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()');
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Origin-Agent-Cluster', '?1');
res.setHeader('X-DNS-Prefetch-Control', 'off');
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
res.setHeader('Content-Security-Policy', csp.join('; '));
if (secureBase || req.secure) res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
if (SENSITIVE_PATH_RE.test(req.path)) {
res.setHeader('Cache-Control', 'no-store, max-age=0');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
}
next();
}
function mutationGuard(req, res, next) {
if (!MUTATING_METHODS.has(req.method)) return next();
const origin = req.get('origin');
const fetchSite = String(req.get('sec-fetch-site') || '').toLowerCase();
if ((origin && !isAllowedOrigin(origin)) || fetchSite === 'cross-site') {
return res.status(403).json({ success: false, message: 'Cross-site request rejected.' });
}
return next();
}
function blockSensitiveStatic(req, res, next) {
if (BLOCKED_STATIC_RE.test(req.path)) return res.status(403).send('Forbidden');
return next();
}
return {
allowedOrigins: new Set(allowedOrigins),
isAllowedOrigin,
headers,
mutationGuard,
blockSensitiveStatic,
corsOptions: {
origin(origin, callback) { callback(null, isAllowedOrigin(origin)); },
credentials: true,
methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type'],
maxAge: 600,
},
};
}
module.exports = {
MUTATING_METHODS,
SENSITIVE_PATH_RE,
BLOCKED_STATIC_RE,
DEFAULT_CSP,
buildAllowedOrigins,
createWebSecurity,
};