5.0 KiB
5.0 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| security-csp-headers | CSP / 보안 헤더 — XSS / Clickjacking 방어 | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
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)
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 발급
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 });
<script nonce="<%= nonce %>">
// OK
</script>
Next.js (next.config.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
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)
<script src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>
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.