109 lines
3.9 KiB
JavaScript
109 lines
3.9 KiB
JavaScript
/**
|
|
* Builds a Content-Security-Policy string with the hard OWASP/Lighthouse-required
|
|
* directives baked in, and opt-in sources for common third parties. Solves the
|
|
* real risk of hand-writing raw CSP per project and forgetting `base-uri 'self'`
|
|
* or `object-src 'none'`.
|
|
*
|
|
* CSP still lives in the project (it lists the project's own domains), but this
|
|
* helper standardizes the skeleton so every project's CSP has the same hardened
|
|
* base — you only flip flags for what the project actually loads.
|
|
*
|
|
* Returns { key, value } ready for buildSecurityHeaders `additional`:
|
|
*
|
|
* import { buildCsp, buildSecurityHeaders } from '@intecion/ipal-kit'
|
|
* const csp = buildCsp({
|
|
* mode: 'report-only', // start here; switch to 'enforce' when clean
|
|
* r2Url: process.env.R2_PUBLIC_URL,
|
|
* turnstile: true, analytics: true,
|
|
* })
|
|
* const headers = buildSecurityHeaders({ hsts: prod, additional: [csp] })
|
|
*
|
|
* Deploy CSP carefully: start with mode:'report-only', check the console for
|
|
* violations across the whole site (forms/Turnstile, gallery/R2, embeds), add
|
|
* missing sources via `extra`, THEN switch to 'enforce'. See docs/security.md.
|
|
*/ export function buildCsp(args = {}) {
|
|
const { analytics, extra = {}, googleMaps, mode = 'enforce', r2Url, turnstile, youtube } = args;
|
|
const src = {
|
|
'default-src': [
|
|
"'self'"
|
|
],
|
|
// 'unsafe-inline' is hard to avoid with Next/analytics; 'unsafe-eval' is NOT
|
|
// added by default (weakens CSP) — add via extra only if a library needs it.
|
|
'connect-src': [
|
|
"'self'"
|
|
],
|
|
'font-src': [
|
|
"'self'",
|
|
'https://fonts.gstatic.com',
|
|
'data:'
|
|
],
|
|
'form-action': [
|
|
"'self'"
|
|
],
|
|
'frame-src': [],
|
|
'img-src': [
|
|
"'self'",
|
|
'data:',
|
|
'blob:'
|
|
],
|
|
'media-src': [],
|
|
'script-src': [
|
|
"'self'",
|
|
"'unsafe-inline'"
|
|
],
|
|
'style-src': [
|
|
"'self'",
|
|
"'unsafe-inline'",
|
|
'https://fonts.googleapis.com'
|
|
],
|
|
'worker-src': [],
|
|
// HARD defaults (OWASP/Lighthouse) — always on, no reason to omit:
|
|
'base-uri': [
|
|
"'self'"
|
|
],
|
|
'frame-ancestors': [
|
|
"'none'"
|
|
],
|
|
'object-src': [
|
|
"'none'"
|
|
]
|
|
};
|
|
if (r2Url) {
|
|
src['img-src'].push(r2Url);
|
|
}
|
|
if (turnstile) {
|
|
src['script-src'].push('https://challenges.cloudflare.com');
|
|
src['frame-src'].push('https://challenges.cloudflare.com');
|
|
src['connect-src'].push('https://challenges.cloudflare.com');
|
|
}
|
|
if (analytics) {
|
|
src['script-src'].push('https://www.googletagmanager.com');
|
|
src['connect-src'].push('https://www.google-analytics.com', 'https://www.googletagmanager.com');
|
|
src['img-src'].push('https://www.google-analytics.com', 'https://www.googletagmanager.com');
|
|
}
|
|
if (youtube) {
|
|
src['frame-src'].push('https://www.youtube.com', 'https://www.youtube-nocookie.com');
|
|
}
|
|
if (googleMaps) {
|
|
src['frame-src'].push('https://www.google.com', 'https://maps.google.com');
|
|
src['script-src'].push('https://maps.googleapis.com');
|
|
src['img-src'].push('https://maps.gstatic.com', 'https://*.googleapis.com');
|
|
}
|
|
// Merge caller extras.
|
|
for (const [dir, values] of Object.entries(extra)){
|
|
if (values && values.length) {
|
|
src[dir] = [
|
|
...src[dir] ?? [],
|
|
...values
|
|
];
|
|
}
|
|
}
|
|
const value = Object.entries(src).filter(([, values])=>values.length > 0).map(([dir, values])=>`${dir} ${values.join(' ')}`).join('; ');
|
|
const key = mode === 'report-only' ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
|
|
return {
|
|
key,
|
|
value
|
|
};
|
|
}
|
|
|
|
//# sourceMappingURL=buildCsp.js.map
|