mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat(security): enhance Content Security Policy and add global security headers
This commit is contained in:
@@ -13,6 +13,64 @@ function resolveBunnyCdnHostname(): string | null {
|
||||
}
|
||||
|
||||
const bunnyCdnHostname = resolveBunnyCdnHostname();
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
// Build Content-Security-Policy from resolved config
|
||||
const cdnOrigin = bunnyCdnHostname ? `https://${bunnyCdnHostname}` : '';
|
||||
|
||||
const connectSrcParts = [
|
||||
"'self'",
|
||||
'https://video.bunnycdn.com',
|
||||
cdnOrigin,
|
||||
// Allow Next.js HMR websocket in development
|
||||
...(isDev ? ['ws://localhost:* wss://localhost:*'] : []),
|
||||
].filter(Boolean);
|
||||
|
||||
const imgSrcParts = [
|
||||
"'self'",
|
||||
'data:',
|
||||
'blob:',
|
||||
'https://img.youtube.com',
|
||||
'https://i.ytimg.com',
|
||||
'https://images.unsplash.com',
|
||||
'https://vz-thumbnail.b-cdn.net',
|
||||
cdnOrigin,
|
||||
].filter(Boolean);
|
||||
|
||||
const mediaSrcParts = ["'self'", 'blob:', cdnOrigin].filter(Boolean);
|
||||
|
||||
const contentSecurityPolicy = [
|
||||
"default-src 'self'",
|
||||
// 'unsafe-inline' is required by Next.js App Router (hydration scripts, inline styles)
|
||||
// https://www.youtube.com is required for the dynamically-injected YouTube IFrame API script
|
||||
"script-src 'self' 'unsafe-inline' https://www.youtube.com",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
`img-src ${imgSrcParts.join(' ')}`,
|
||||
`media-src ${mediaSrcParts.join(' ')}`,
|
||||
"frame-src 'self' https://www.youtube.com https://iframe.mediadelivery.net",
|
||||
`connect-src ${connectSrcParts.join(' ')}`,
|
||||
// next/font self-hosts Google Fonts at build time — no external font origin needed
|
||||
"font-src 'self'",
|
||||
"worker-src 'self' blob:",
|
||||
"object-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
"frame-ancestors 'none'",
|
||||
].join('; ');
|
||||
|
||||
const securityHeaders = [
|
||||
{ key: 'Content-Security-Policy', value: contentSecurityPolicy },
|
||||
// Prevent the app from being embedded in foreign iframes (clickjacking)
|
||||
{ key: 'X-Frame-Options', value: 'DENY' },
|
||||
// Prevent MIME-type sniffing on all responses
|
||||
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
||||
// Enforce HTTPS for 2 years
|
||||
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains' },
|
||||
// microphone=(self) is required for audio comment recording; camera/geolocation unused
|
||||
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(self), geolocation=()' },
|
||||
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
||||
];
|
||||
|
||||
const remotePatterns: RemotePattern[] = [
|
||||
{ protocol: 'https', hostname: 'img.youtube.com' },
|
||||
{ protocol: 'https', hostname: 'i.ytimg.com' },
|
||||
@@ -34,6 +92,11 @@ const nextConfig: NextConfig = {
|
||||
compress: true,
|
||||
async headers() {
|
||||
return [
|
||||
// Global security headers applied to every response
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: securityHeaders,
|
||||
},
|
||||
{
|
||||
source: '/_next/static/:path*',
|
||||
headers: [
|
||||
|
||||
Reference in New Issue
Block a user