Files
2nd/10_Wiki/Topics/Coding/Security_CSP_Headers.md
T
2026-05-09 21:08:02 +09:00

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
security
csp
headers
web
vibe-coding
language applicable_to
TS / HTTP
Frontend
Backend
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)

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.

🔗 관련 문서