108 lines
3.0 KiB
JavaScript
108 lines
3.0 KiB
JavaScript
function normalizeAssetPrefix(rawValue, basePath) {
|
|
if (!rawValue) return undefined
|
|
|
|
try {
|
|
const url = new URL(rawValue)
|
|
const pathname = url.pathname.replace(/\/$/, '')
|
|
if (!pathname.endsWith(basePath)) {
|
|
url.pathname = `${pathname}${basePath}`
|
|
}
|
|
return url.toString().replace(/\/$/, '')
|
|
} catch {
|
|
const trimmed = rawValue.replace(/\/$/, '')
|
|
return trimmed.endsWith(basePath) ? trimmed : `${trimmed}${basePath}`
|
|
}
|
|
}
|
|
|
|
function collectOrigins(assetSources) {
|
|
const origins = new Set()
|
|
|
|
for (const source of assetSources) {
|
|
if (!source) continue
|
|
|
|
try {
|
|
origins.add(new URL(source).origin)
|
|
} catch {
|
|
continue
|
|
}
|
|
}
|
|
|
|
return [...origins]
|
|
}
|
|
|
|
function collectWebSocketOrigins(assetSources) {
|
|
const origins = new Set()
|
|
|
|
for (const source of assetSources) {
|
|
if (!source) continue
|
|
|
|
try {
|
|
const url = new URL(source)
|
|
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
origins.add(`${protocol}//${url.host}`)
|
|
} catch {
|
|
continue
|
|
}
|
|
}
|
|
|
|
return [...origins]
|
|
}
|
|
|
|
function collectBrowserApiSources() {
|
|
const sources = [process.env.NEXT_PUBLIC_API_URL, process.env.API_URL]
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
sources.push('http://localhost:4000/api/v1')
|
|
}
|
|
|
|
return sources
|
|
}
|
|
|
|
function buildSecurityHeaders({ assetSources = [], connectSources = collectBrowserApiSources() } = {}) {
|
|
const assetOrigins = collectOrigins(assetSources)
|
|
const connectOrigins = collectOrigins(connectSources)
|
|
const websocketOrigins = [
|
|
...collectWebSocketOrigins(assetSources),
|
|
...collectWebSocketOrigins(connectSources),
|
|
]
|
|
|
|
const scriptSrc = ["'self'", "'unsafe-inline'"]
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
scriptSrc.push("'unsafe-eval'")
|
|
}
|
|
scriptSrc.push(...assetOrigins)
|
|
|
|
const styleSrc = ["'self'", "'unsafe-inline'", ...assetOrigins]
|
|
const imgSrc = [...new Set(["'self'", 'data:', 'blob:', 'https:', ...assetOrigins, ...connectOrigins])]
|
|
const fontSrc = ["'self'", 'data:', ...assetOrigins]
|
|
const connectSrc = [...new Set(["'self'", 'https:', 'wss:', ...assetOrigins, ...connectOrigins, ...websocketOrigins])]
|
|
|
|
return [
|
|
{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
|
|
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
{ key: 'X-Frame-Options', value: 'DENY' },
|
|
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
|
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
|
|
{
|
|
key: 'Content-Security-Policy',
|
|
value: [
|
|
"default-src 'self'",
|
|
`script-src ${scriptSrc.join(' ')}`,
|
|
`style-src ${styleSrc.join(' ')}`,
|
|
`img-src ${imgSrc.join(' ')}`,
|
|
`font-src ${fontSrc.join(' ')}`,
|
|
`connect-src ${connectSrc.join(' ')}`,
|
|
"frame-ancestors 'none'",
|
|
"base-uri 'self'",
|
|
"form-action 'self'",
|
|
"object-src 'none'",
|
|
].join('; '),
|
|
},
|
|
]
|
|
}
|
|
|
|
module.exports = {
|
|
buildSecurityHeaders,
|
|
normalizeAssetPrefix,
|
|
}
|