import { getRequestURL, setResponseHeader } from 'h3' const enforcedContentSecurityPolicy = [ "base-uri 'self'", "object-src 'none'", "frame-ancestors 'self'" ].join('; ') const contentSecurityPolicyReportOnly = [ "default-src 'self' https: data: blob:", "base-uri 'self'", "object-src 'none'", "frame-ancestors 'self'", "script-src 'self' 'unsafe-inline' 'unsafe-eval' https:", "style-src 'self' 'unsafe-inline' https:", "img-src 'self' data: blob: https:", "font-src 'self' data: https:", "media-src 'self' blob: https:", "frame-src https:", "connect-src 'self' https: wss:", "worker-src 'self' blob: https:", "manifest-src 'self' https:", 'report-uri /api/csp-report', 'report-to csp-endpoint' ].join('; ') /** * 현재 요청과 같은 origin의 CSP 보고 URL을 안전하게 만든다. * @param {import('h3').H3Event} event - 요청 이벤트 * @returns {string} CSP 보고 URL */ const getCspReportEndpoint = (event) => { try { const requestUrl = getRequestURL(event) if (!['http:', 'https:'].includes(requestUrl.protocol)) { return '/api/csp-report' } const endpoint = new URL('/api/csp-report', requestUrl.origin).toString() return /["\\\r\n]/.test(endpoint) ? '/api/csp-report' : endpoint } catch { return '/api/csp-report' } } /** * 공통 보안 헤더와 호환성 관찰 CSP를 적용한다. * @param {import('h3').H3Event} event - 요청 이벤트 * @param {{ enforceCsp?: boolean }} options - 강제 CSP 적용 옵션 * @returns {void} */ export const applySecurityHeaders = (event, options = {}) => { const reportEndpoint = getCspReportEndpoint(event) setResponseHeader(event, 'X-Content-Type-Options', 'nosniff') setResponseHeader(event, 'Referrer-Policy', 'strict-origin-when-cross-origin') setResponseHeader(event, 'X-Frame-Options', 'SAMEORIGIN') setResponseHeader(event, 'Permissions-Policy', 'camera=(), microphone=(), geolocation=()') setResponseHeader(event, 'Reporting-Endpoints', `csp-endpoint="${reportEndpoint}"`) setResponseHeader(event, 'Report-To', JSON.stringify({ group: 'csp-endpoint', max_age: 10886400, endpoints: [{ url: reportEndpoint }] })) setResponseHeader(event, 'Content-Security-Policy-Report-Only', contentSecurityPolicyReportOnly) if (options.enforceCsp !== false) { setResponseHeader(event, 'Content-Security-Policy', enforcedContentSecurityPolicy) } }