--- id: security-csp-headers title: CSP / 보안 헤더 — XSS / Clickjacking 방어 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [security, csp, headers, web, vibe-coding] tech_stack: { language: "TS / HTTP", applicable_to: ["Frontend", "Backend"] } applied_in: [] aliases: [CSP, Content-Security-Policy, nonce, HSTS, X-Frame-Options, helmet] --- # CSP / 보안 헤더 > XSS 방어 마지막 보호막 = **Content-Security-Policy**. 인라인 script 차단 + nonce. **HSTS / X-Frame-Options / Permissions-Policy** 같이 묶어서 helmet 으로. ## 📖 핵심 개념 - CSP: 어떤 origin 의 자원만 로드 가능한지 명시. - nonce: 매 요청마다 random — 인라인 script 1회 통과. - Strict CSP: `'strict-dynamic'` + nonce — host allowlist 안 필요. - Report-only: 위반 보고만, 차단 X (점진 도입). ## 💻 코드 패턴 ### Helmet (Express) ```ts import helmet from 'helmet'; app.use(helmet({ contentSecurityPolicy: { useDefaults: false, directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", `'nonce-${res.locals.cspNonce}'`, "'strict-dynamic'"], styleSrc: ["'self'", "'unsafe-inline'"], // CSS 는 보통 강 X imgSrc: ["'self'", 'data:', 'https:'], connectSrc: ["'self'", 'https://api.example.com'], fontSrc: ["'self'", 'https://fonts.gstatic.com'], objectSrc: ["'none'"], frameAncestors: ["'none'"], baseUri: ["'self'"], formAction: ["'self'"], }, }, crossOriginOpenerPolicy: { policy: 'same-origin' }, crossOriginResourcePolicy: { policy: 'same-origin' }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }, })); ``` ### nonce 발급 ```ts import { randomBytes } from 'node:crypto'; app.use((req, res, next) => { res.locals.cspNonce = randomBytes(16).toString('base64'); next(); }); // 템플릿 res.render('index', { nonce: res.locals.cspNonce }); ``` ```html ``` ### Next.js (next.config.js) ```js const cspHeader = ` default-src 'self'; script-src 'self' 'nonce-NONCE' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; `.replace(/\s{2,}/g, ' ').trim(); module.exports = { async headers() { return [{ source: '/:path*', headers: [ { key: 'Content-Security-Policy', value: cspHeader }, { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains; preload' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, { key: 'X-Frame-Options', value: 'DENY' }, ], }]; }, }; ``` ### CSP report-only (점진) ``` Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report ``` ```ts app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => { log.warn('csp violation', req.body['csp-report']); res.status(204).end(); }); ``` ### Permissions-Policy (sensors / 권한) ``` Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=() ``` ### HSTS (HTTPS 강제) ``` Strict-Transport-Security: max-age=31536000; includeSubDomains; preload ``` `preload`: hstspreload.org 등록 시 브라우저 hardcoded list 에 포함. ### Subresource Integrity (CDN script) ```html ``` CDN 가 변조되면 브라우저가 차단. ### X-Frame-Options vs frame-ancestors - `X-Frame-Options: DENY` (legacy) - CSP `frame-ancestors 'none'` (modern, 우선) ## 🤔 의사결정 기준 | 헤더 | 우선 | |---|---| | 모든 사이트 | HSTS + X-Content-Type-Options + Referrer-Policy + Permissions-Policy | | XSS 방어 강 | CSP strict + nonce | | Iframe 사용 안 함 | frame-ancestors 'none' | | 외부 cdn 의존 | SRI integrity | | SaaS 임베드 가능 필요 | frame-ancestors specific origin | | 점진 도입 | Report-only → enforce | ## ❌ 안티패턴 - **`'unsafe-inline'` 그대로**: CSP 의미 절반 잃음. nonce. - **`*` 허용**: CSP 무의미. - **Report 검토 안 함**: 위반 모름. - **HSTS 없음 (HTTP 가능)**: SSL strip. - **Self-signed cert prod**: HSTS 못 씀. - **X-XSS-Protection 1**: 옛 헤더, modern X. 빼기. - **Helmet 끄기 — 빠르다고**: 기본은 거의 무료. - **Hash 만 + dynamic script**: 매 변경 hash 갱신 필요. ## 🤖 LLM 활용 힌트 - Helmet defaults 그대로 좋다. - CSP = nonce + strict-dynamic. - Report-only 로 시작 → enforce. ## 🔗 관련 문서 - [[Security_OWASP_Top_10_Practical]] - [[Web_CORS_Practical_Guide]] - [[Security_Output_Encoding_XSS]]