# 06. 웹 보안 심화 가이드

## 0. 먼저 알고 가기 (30초 요약)

- 인증은 `누가`인지, 인가는 `무엇을` 할 수 있는지부터 체크하세요.
- 입력/출력 단에서 기본 안전장치를 먼저 넣고, 나중에 규칙을 강화하세요.
- 보안은 한번에 완성되기보다, 로그·감사·차단을 통해 계속 보정합니다.

## 초심자용 한눈에 보기

보안은 “무엇이 위험한지”보다 “실수했을 때도 깨지 않도록” 관리합니다.

### 핵심 용어 빠르게 정리

| 용어            | 쉬운 뜻                                                   |
| --------------- | --------------------------------------------------------- |
| `인증`          | 누군지 확인하는 과정                                      |
| `인가`          | 어떤 권한을 할당할지 결정하는 과정                        |
| `CSP`           | 브라우저에서 실행할 수 있는 스크립트/자원을 제한하는 규칙 |
| `토큰`          | 로그인/권한 확인에 쓰는 짧은 암호화 문자열                |
| `trusted types` | DOM에 들어가는 위험한 문자열 주입을 줄이는 브라우저 기능  |

| 분류            | 아키텍처                                                                                                                            | 상태          | Stable    |
| :-------------- | :---------------------------------------------------------------------------------------------------------------------------------- | :------------ | :-------- |
| **연관 가이드** | [10. 인프라](./10_인프라_IaC_가이드.md), [05. API 통신](./05_API_통신_및_모킹_가이드.md), [11. CI/CD](./11_CICD_파이프라인_표준.md) | **도구 원칙** | 벤더 중립 |
| **핵심 테마**   | OWASP Top 10:2025, CSP3 + Trusted Types, Passkeys/WebAuthn, RFC 9700 OAuth 2.0 Security BCP, Supply Chain                           | **Update**    | 최신 기준 |

---

> **"보안은 기능이 아니라 제품의 전제 조건이다. 보이지 않는 곳에서의 방어 체계가 서비스의 가치를 지킨다."**
> 본 가이드는 현대 프론트엔드 환경에서 발생할 수 있는 주요 보안 위협과 그에 대한 철저한 방어 표준을 제시합니다.

---

## 추천 항목 (실무 우선순위)

- **시작 추천**: 인증·권한 체계를 분리하고 토큰 저장 위치와 만료 정책을 먼저 맞춥니다.
- **안정 추천**: CSP, CORS, 보안 헤더 기본값을 템플릿으로 강제화해 누락을 막습니다.
- **운영 추천**: 취약점 스캐닝과 secret 노출 점검을 PR 템플릿의 필수 항목으로 만듭니다.

## 추천 항목 고도화 체크

- `첫 적용` — CSP, 인증, 시크릿, 공급망 통제 중 하나를 실제 PR이나 운영 이슈에 붙이고, 변경 전 기준을 먼저 적는다.
- `증거 정리` — security scan, header capture, secretlint 결과, SBOM/attestation를 같은 작업 기록에 남긴다.
- `재점검` — 차단된 secret 수, CSP report, 취약 dependency 수가 나아졌는지 30일 안에 확인하고 기준을 유지, 수정, 폐기 중 하나로 판정한다.

## 추천 항목 실행 기록 템플릿

- `작업` : CSP, 인증, 시크릿, 공급망 통제 적용 범위를 어느 화면, 패키지, 문서에 둘지 적는다.
- `증거` : security scan, header capture, secretlint 결과, SBOM/attestation 중 실제로 남긴 항목만 링크한다.
- `판정` : 유지/수정/폐기 중 하나와 이유를 한 문장으로 남긴다.
- `다음 점검` : 차단된 secret 수, CSP report, 취약 dependency 수를 다시 볼 날짜와 담당자를 지정한다.

## 문서 책임 범위

| 이 문서가 결정하는 것                                                    | 단일 출처로 따르는 문서                                                                  |
| :----------------------------------------------------------------------- | :--------------------------------------------------------------------------------------- |
| 브라우저 보안 모델, CSP, Trusted Types, auth/session, client secret 기준 | [05. API 통신](./05_API_통신_및_모킹_가이드.md), [10. 인프라](./10_인프라_IaC_가이드.md) |
| supply chain, lockfile, SBOM, artifact provenance 보안 기준              | [11. CI/CD](./11_CICD_파이프라인_표준.md)                                                |
| 보안 이벤트 탐지와 incident escalation                                   | [09. 관측성](./09_장애_대응_및_관측성_표준.md)                                           |
| AI 입력에 포함하면 안 되는 데이터와 보안 검증 책임                       | [18. AI 개발 워크플로우](./18_AI_개발_워크플로우_종합.md)                                |

---

## 0. 모든 프론트엔드 그룹 공통 Baseline

보안 표준은 특정 보안 제품이 아니라 **브라우저 보안 모델, 공급망 무결성, 인증 경계, 운영 탐지**를 함께 통제하는 체계입니다.

| 기준              | 최소 적용                                                                                      |
| :---------------- | :--------------------------------------------------------------------------------------------- |
| **위협 모델**     | 신규 인증/결제/관리자/외부 입력 기능은 STRIDE 또는 동등한 모델로 위험을 기록합니다.            |
| **브라우저 방어** | CSP, Trusted Types(가능한 경우), 보안 헤더, 안전한 URL 검증을 기본값으로 둡니다.               |
| **공급망 무결성** | lockfile 고정, provenance/attestation, dependency audit, license check를 CI에 둡니다.          |
| **비밀 관리**     | client bundle에 secret을 포함하지 않고, public env와 server secret을 분리합니다.               |
| **탐지와 대응**   | auth failure, CSP violation, unusual API error, dependency alert를 관측성 시스템으로 보냅니다. |

### 0.0 보안 방어 레이어

```mermaid
flowchart TD
  A[사용자 입력/요청] --> B{브라우저 보안 경계}
  B -->|출처·형식 검증| C[Validation & Sanitize]
  B -->|리소스 실행 검증| D[CSP / Trusted Types]
  C --> E[인증/인가 + API 호출]
  D --> E
  E --> F[관측성 수집]
  F --> G{이상 징후}
  G -->|감지| H[알림 + 즉시 완화 + 사고 대응]
  G -->|정상| I[안전 종료]
  H --> J[사후 분석 + 룰/정책 갱신]
```

### 0.1 교차 검증 매트릭스

| 권고                                                                | 1차 출처                                              | 실행 증거                                  | 운영 증거                              | 철회 조건                                                              |
| :------------------------------------------------------------------ | :---------------------------------------------------- | :----------------------------------------- | :------------------------------------- | :--------------------------------------------------------------------- |
| OWASP Top 10:2025를 보안 체크리스트의 상위 분류로 사용한다          | OWASP Top 10:2025 공식 문서                           | threat model, SAST/DAST rule mapping       | 보안 사고 분류, vulnerability aging    | 프로젝트 도메인이 OWASP ASVS/MASTG 등 더 구체적 표준을 요구할 때       |
| CSP는 report-only에서 시작해 nonce/hash 기반 enforcement로 승격한다 | CSP Level 3, MDN/W3C 문서                             | CSP regression test, violation report 검증 | violation rate, XSS near miss          | 레거시 third-party script가 정책을 만족하지 못해 임시 예외가 필요할 때 |
| dependency와 build artifact는 출처/무결성을 증명한다                | SLSA v1.2, NIST SSDF, package manager provenance 문서 | lockfile audit, SBOM/provenance artifact   | dependency incident MTTR               | 폐쇄망/레거시 registry가 provenance를 지원하지 않을 때                 |
| 인증은 토큰 저장 위치, 회전, 재인증 UX를 함께 설계한다              | RFC 9700 OAuth 2.0 Security BCP, WebAuthn             | auth flow E2E, CSRF/XSS 테스트             | 401/403 spike, account takeover signal | 위협 모델상 공개 읽기 전용 앱일 때                                     |

---

### 0.2 운영 게이트

| Gate                 | Evidence                                  | Owner          | Rollback                                 |
| :------------------- | :---------------------------------------- | :------------- | :--------------------------------------- |
| Threat model gate    | STRIDE/ASVS checklist, risk owner         | Security owner | feature flag off 또는 launch hold        |
| CSP enforcement gate | report-only baseline, violation burn-down | Platform owner | report-only로 되돌리고 예외 ADR 작성     |
| Supply-chain gate    | lockfile audit, SBOM/provenance artifact  | Build owner    | dependency pinning 또는 package rollback |
| Auth/session gate    | CSRF/XSS/auth E2E, token rotation test    | Auth owner     | previous session strategy 유지           |

---

### 0.3 OWASP Top 10:2025 — 무엇이 달라졌나

OWASP Top 10:2025는 A03에 **Software Supply Chain Failures**를 추가하고, A10에 **Mishandling of Exceptional Conditions**를 추가했습니다. 프론트엔드는 이제 XSS와 인증뿐 아니라 패키지 공급망, CI/CD, 예외 처리, 보안 로깅을 같은 수준의 보안 표준으로 다뤄야 합니다.

| 순위         | 2025 카테고리                                    | 2021 대비 변화                                              |
| :----------- | :----------------------------------------------- | :---------------------------------------------------------- |
| **A01:2025** | Broken Access Control                            | 1위 유지. **SSRF가 이 항목으로 흡수**됨                     |
| **A02:2025** | Security Misconfiguration                        | #5 → **#2** (전 애플리케이션에서 발견)                      |
| **A03:2025** | **Software Supply Chain Failures** ⭐ NEW        | 기존 "Vulnerable Components"의 확장(빌드 시스템·CI/CD 포함) |
| **A04:2025** | Cryptographic Failures                           | #2 → #4                                                     |
| **A05:2025** | Injection                                        | #3 → #5                                                     |
| **A06:2025** | Insecure Design                                  | 유지                                                        |
| **A07:2025** | Authentication Failures                          | "Identification and Authentication Failures"에서 리네이밍   |
| **A08:2025** | Software or Data Integrity Failures              | 유지                                                        |
| **A09:2025** | Security Logging and Alerting Failures           | "Monitoring" 빠지고 "Alerting" 강조                         |
| **A10:2025** | **Mishandling of Exceptional Conditions** ⭐ NEW | 예외 처리·오류 흐름 미숙으로 발생하는 보안 사고             |

**프론트엔드 관점에서 주목할 변화 3가지**

1. **A02(Misconfiguration) 급상승** — CSP·CORS·보안 헤더 누락이 가장 흔한 침투 경로로 재확인되었습니다. 본 가이드 8번 섹션의 헤더 체크리스트를 반드시 자동화하세요.
2. **A03(Supply Chain) 신설** — npm 패키지·빌드 산출물·CI 토큰 전반의 무결성이 별도 카테고리로 분리되었습니다. 6번 섹션의 lockfile·provenance·`npm audit signatures`가 더 중요해졌습니다.
3. **A10(예외 처리)** — 401/403/500 응답에 스택 트레이스·내부 경로가 노출되는 사례가 정식 항목이 되었습니다. 9.2의 프로덕션 에러 핸들러 패턴이 핵심입니다.

---

### 0.4 인증·공급망 공식 표준 해석

보안 문서에서 버전명과 표준 상태가 섞이면 잘못된 구현 결정을 만들 수 있습니다. 현재 공식 문서 기준 OAuth 2.1은 RFC가 아니라 개발 중인 통합 문서이므로, production 보안 기준은 **RFC 9700 OAuth 2.0 Security BCP**와 각 인증 제공자의 OIDC 문서로 판단합니다.

| 주제              | production 기준                                                                                 | 실행 증거                                         | 주의할 점                                              |
| :---------------- | :---------------------------------------------------------------------------------------------- | :------------------------------------------------ | :----------------------------------------------------- |
| OAuth/OIDC        | Authorization Code + PKCE, exact redirect URI, `state`/`nonce`, issuer 검증, implicit/ROPC 회피 | auth E2E, redirect URI test, token storage review | “OAuth 2.1”이라고만 쓰지 말고 근거 RFC/프로파일을 명시 |
| Token replay 방지 | refresh token rotation 또는 sender-constrained token, audience/scope 최소화                     | rotation test, replay rejection log               | SPA client secret은 secret이 아님                      |
| JWT 검증          | RFC 8725 기준 알고리즘 allowlist, `iss`/`aud`/`exp`/`kid` 검증                                  | negative token test                               | `alg: none`, key confusion, 과도한 만료 시간 금지      |
| Supply chain      | SLSA provenance, SBOM, lockfile, CI 최소 권한, artifact 검증                                    | SBOM/provenance/checksum artifact                 | “audit 통과”만으로 무결성 증명이 되지 않음             |

---

## 1. XSS (Cross-Site Scripting) 방어

### 왜 중요한가

사용자의 입력값이 스크립트로 실행되는 것을 방지합니다. XSS는 가장 빈번하게 발생하는 웹 보안 취약점이며, Reflected / Stored / DOM-based 세 가지 유형이 존재합니다.

> **일상 비유**: 식당 후기 게시판에 "음식 맛이 별로" 대신 "음식 맛이 별로<script>몰래 카드 정보 보내기</script>"를 적어 두면, 다른 손님이 게시판을 볼 때 그 스크립트가 실행됩니다. XSS는 글 속에 숨긴 명령이 다른 사람의 브라우저에서 실행되는 일입니다.

#### XSS 방어 다층 방어 흐름

```mermaid
flowchart TD
  A[사용자 입력] --> B{입력 경로}
  B -- 폼/URL/외부 API --> C[입력 검증 + Zod 스키마]
  C --> D[저장]
  D --> E[출력 시점]
  E --> F{출력 방식}
  F -- JSX 텍스트 --> G[React 자동 escape - 안전]
  F -- dangerouslySetInnerHTML --> H[DOMPurify sanitize]
  F -- href/src 속성 --> I[프로토콜 검증<br/>http/https/mailto만]
  F -- 스크립트 주입 시도 --> J[CSP: script-src nonce/hash]
  H --> K[브라우저 렌더]
  I --> K
  J --> K
  K --> L[Trusted Types: 잘못된 sink 호출 차단]
  L -. 위반 .-> M[CSP report-uri로 위반 보고]
```

### 1.1 React의 기본 XSS 방어와 한계

React는 JSX에서 렌더링되는 값을 자동으로 이스케이프하여 기본적인 XSS를 방어합니다. 하지만 **안전하지 않은 경우**가 존재합니다.

```tsx
// ✅ 안전 — React가 자동으로 이스케이프 처리
const userInput = '<script>alert("xss")</script>'
return <div>{userInput}</div> // 문자열 그대로 출력됨

// ❌ 위험 — dangerouslySetInnerHTML은 이스케이프를 우회
return <div dangerouslySetInnerHTML={{ __html: userInput }} />

// ❌ 위험 — href에 javascript: 프로토콜 주입 가능
const maliciousUrl = 'javascript:alert("xss")'
return <a href={maliciousUrl}>Click</a>

// ✅ 안전한 URL 검증 함수
function isSafeUrl(url: string): boolean {
  try {
    const parsed = new URL(url)
    return ['http:', 'https:', 'mailto:'].includes(parsed.protocol)
  } catch {
    return false
  }
}

// ❌ 위험 — eval, Function 생성자, innerHTML 직접 조작
eval(userInput) // 절대 사용 금지
new Function(userInput)() // 절대 사용 금지
document.getElementById('target')!.innerHTML = userInput // React 외부 DOM 조작 금지
```

### 1.2 CSP (Content Security Policy) 설정

가장 강력한 방어 수단입니다. 브라우저에게 "이 도메인의 스크립트만 실행하라"고 지시합니다.

> **일상 비유**: 공연장 입구에서 발급한 정확한 도장(nonce)이 찍힌 표만 입장을 허용하는 것과 같습니다. 위조 표(주입된 스크립트)는 도장이 없어 거부됩니다.

#### CSP 단계별 적용 로드맵

```mermaid
flowchart LR
  A[Phase 0<br/>CSP 미적용] --> B[Phase 1<br/>Content-Security-Policy-Report-Only]
  B --> C{위반 보고서<br/>분석 1~2주}
  C -- 합법 트래픽 위반 --> D[정책 보완<br/>도메인 화이트리스트]
  D --> B
  C -- 위반 0건 안정 --> E[Phase 2<br/>Enforcement 모드]
  E --> F[Phase 3<br/>nonce 기반 'strict-dynamic']
  F --> G[Phase 4<br/>require-trusted-types-for 'script']
  G --> H[Phase 5<br/>frame-ancestors / form-action 등 강화]
  C -. report-uri .-> R[보안 모니터링<br/>위반 패턴 알림]
```

#### CSP 디렉티브 의존 관계

```mermaid
flowchart TD
  D[default-src<br/>모든 리소스 기본값] --> S[script-src]
  D --> ST[style-src]
  D --> I[img-src]
  D --> C[connect-src]
  D --> F[font-src]
  D --> FR[frame-src]
  S -.-> N1["'nonce-...'<br/>per-request 토큰"]
  S -.-> H1["'strict-dynamic'<br/>nonce된 스크립트가 로드한 것 신뢰"]
  S -.-> WL1[도메인 화이트리스트]
  FA[frame-ancestors<br/>clickjacking 방지] -. 독립 .-> X1
  FORM[form-action<br/>제출 대상 제한] -. 독립 .-> X1
  RTT[require-trusted-types-for] -. 추가 강화 .-> S
  R[report-uri / report-to] -. 위반 수집 .-> X1[브라우저 적용]
```

#### 주요 CSP 디렉티브 설명

| 디렉티브                    | 설명                                                   | 예시                                  |
| :-------------------------- | :----------------------------------------------------- | :------------------------------------ |
| `default-src`               | 모든 리소스의 기본 정책 (다른 디렉티브가 없을 때 폴백) | `'self'`                              |
| `script-src`                | JavaScript 소스 제한                                   | `'self' 'nonce-abc123'`               |
| `style-src`                 | CSS 소스 제한                                          | `'self' 'unsafe-inline'`              |
| `img-src`                   | 이미지 소스 제한                                       | `'self' data: https:`                 |
| `connect-src`               | XHR, Fetch, WebSocket 연결 대상 제한                   | `'self' https://api.example.com`      |
| `font-src`                  | 웹 폰트 소스 제한                                      | `'self' https://font-cdn.example.com` |
| `frame-src`                 | iframe 소스 제한                                       | `'none'`                              |
| `object-src`                | `<object>`, `<embed>` 제한 (Flash 등)                  | `'none'`                              |
| `base-uri`                  | `<base>` 태그의 URL 제한                               | `'self'`                              |
| `form-action`               | 폼 제출 대상 제한                                      | `'self'`                              |
| `frame-ancestors`           | 이 페이지를 iframe으로 삽입할 수 있는 부모 제한        | `'none'`                              |
| `upgrade-insecure-requests` | HTTP 요청을 HTTPS로 자동 업그레이드                    | (값 없음)                             |

#### Meta 태그 방식 (간단한 프로젝트)

```html
<!-- index.html 예시 -->
<meta
  http-equiv="Content-Security-Policy"
  content="
  default-src 'self';
  script-src 'self' https://trusted-api.com;
  style-src 'self' 'unsafe-inline' https://style-cdn.example.com;
  img-src 'self' data: https://images.unsplash.com;
  connect-src 'self' https://api.myserver.com;
  font-src 'self' https://font-cdn.example.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;
"
/>
```

#### Nonce 기반 CSP (Next.js)

`'unsafe-inline'`을 사용하지 않고, 요청마다 고유한 nonce를 생성하여 인라인 스크립트를 허용하는 방식입니다.

```typescript
// middleware.ts — Next.js 미들웨어에서 nonce 생성
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'

export function middleware(request: NextRequest) {
  // 요청마다 고유한 nonce 생성
  const nonce = crypto.randomBytes(16).toString('base64')

  // CSP 헤더 구성 — nonce 기반으로 인라인 스크립트 허용
  const cspHeader = [
    `default-src 'self'`,
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'nonce-${nonce}' https://style-cdn.example.com`,
    `img-src 'self' data: https:`,
    `connect-src 'self' https://api.myserver.com`,
    `font-src 'self' https://font-cdn.example.com`,
    `object-src 'none'`,
    `base-uri 'self'`,
    `form-action 'self'`,
    `frame-ancestors 'none'`,
    `upgrade-insecure-requests`,
  ].join('; ')

  const response = NextResponse.next()

  // 응답 헤더에 CSP 설정
  response.headers.set('Content-Security-Policy', cspHeader)
  // nonce를 하위 컴포넌트에서 사용할 수 있도록 헤더로 전달
  response.headers.set('x-nonce', nonce)

  return response
}
```

```tsx
// app/layout.tsx — nonce를 Script 태그에 전달
import { headers } from 'next/headers'
import Script from 'next/script'

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const headersList = await headers()
  const nonce = headersList.get('x-nonce') ?? ''

  return (
    <html lang="ko">
      <body>
        {children}
        {/* nonce가 포함된 인라인 스크립트만 실행 허용 */}
        <Script nonce={nonce} strategy="afterInteractive">
          {`console.log("이 스크립트는 nonce로 인증되어 실행됩니다.");`}
        </Script>
      </body>
    </html>
  )
}
```

### 1.3 Trusted Types — DOM XSS의 근본 차단 (CSP Level 3)

`Trusted Types`는 `innerHTML`, `document.write`, `eval` 등 **DOM XSS 싱크(sink)** 에 일반 문자열을 넣는 것을 브라우저 차원에서 금지합니다. 정책에서 생성된 안전한 타입(`TrustedHTML`, `TrustedScript`, `TrustedScriptURL`)만 허용되므로, 새니타이저를 거치지 않은 입력이 실행될 가능성이 원천 차단됩니다. 현재 공식 문서 기준 Chrome·Edge·Opera에 안정 적용되어 있고, Safari·Firefox는 플래그/실험 단계로 지원합니다.

> **일상 비유**: 약국에서 처방전을 통과한 약(TrustedHTML)만 카운터 너머로 받을 수 있고, 누가 가져온 일반 봉투(문자열)는 받지 않는 규칙과 같습니다.

#### Trusted Types 처리 단계

```mermaid
sequenceDiagram
  participant App as 애플리케이션 코드
  participant Policy as trustedTypes.createPolicy('app-policy')
  participant Sanitizer as DOMPurify / allowlist
  participant Sink as innerHTML / script src
  participant Browser as 브라우저

  App->>Policy: createHTML(userInput)
  Policy->>Sanitizer: sanitize(userInput)
  Sanitizer-->>Policy: cleanHTML
  Policy-->>App: TrustedHTML 인스턴스
  App->>Sink: element.innerHTML = TrustedHTML
  Sink->>Browser: 안전한 HTML 삽입
  alt 잘못된 호출
    App->>Sink: element.innerHTML = "<script>...</script>" (raw string)
    Sink->>Browser: 거부 + TypeError
    Browser-->>App: CSP 위반 보고
  end
```

```http
# 강제 모드 — 정책에서 생성되지 않은 값은 모두 거부
Content-Security-Policy:
  require-trusted-types-for 'script';
  trusted-types default app-policy;
```

```typescript
// app/trusted-types.ts — 앱이 사용할 정책 정의 (반드시 첫 로드 전에 실행)
import DOMPurify from 'dompurify'

if (window.trustedTypes && trustedTypes.createPolicy) {
  trustedTypes.createPolicy('app-policy', {
    // 모든 HTML 주입은 DOMPurify를 통과해야만 TrustedHTML이 됨
    createHTML: (input: string) => DOMPurify.sanitize(input),
    // 스크립트 URL 화이트리스트
    createScriptURL: (url: string) => {
      const allowed = ['https://cdn.example.com/', 'https://analytics.example.com/']
      if (allowed.some((prefix) => url.startsWith(prefix))) return url
      throw new Error(`허용되지 않은 스크립트 URL: ${url}`)
    },
  })
}
```

> **단계적 도입 팁**: 즉시 차단이 부담스럽다면 먼저 `Content-Security-Policy-Report-Only`로 위반 보고서만 수집하세요. 의존 라이브러리(예: 일부 차트/에디터)가 미준수일 수 있으므로, 충돌하는 라이브러리를 모두 확인한 후 강제 모드로 전환합니다.

### 1.4 데이터 새니타이징 (Sanitization)

`dangerouslySetInnerHTML` 사용은 원칙적으로 금지하며, 불가피한 경우 `DOMPurify`를 사용합니다.

#### 기본 사용법

```typescript
import DOMPurify from "dompurify";

const cleanHTML = DOMPurify.sanitize(userInputHTML);
return <div dangerouslySetInnerHTML={{ __html: cleanHTML }} />;
```

#### 상세 설정 옵션

```typescript
import DOMPurify from 'dompurify'

// 허용할 태그와 속성을 명시적으로 제한
const cleanHTML = DOMPurify.sanitize(dirtyHTML, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li', 'h1', 'h2', 'h3'],
  ALLOWED_ATTR: ['href', 'target', 'rel', 'class'],
  ALLOW_DATA_ATTR: false, // data-* 속성 차단
  ADD_ATTR: ['target'], // 추가 허용 속성
  FORBID_TAGS: ['script', 'style', 'iframe', 'form', 'input'], // 명시적 차단
  FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'], // 이벤트 핸들러 차단
})

// 커스텀 훅으로 재사용
function useSanitizedHTML(dirty: string) {
  return useMemo(
    () =>
      DOMPurify.sanitize(dirty, {
        ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
        ALLOWED_ATTR: ['href', 'target', 'rel'],
        // 모든 href에 자동으로 rel="noopener noreferrer" 추가
        ADD_ATTR: ['rel'],
      }),
    [dirty],
  )
}

// DOMPurify 훅을 활용한 링크 보안 강화
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
  // 모든 외부 링크에 noopener noreferrer 추가
  if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {
    node.setAttribute('rel', 'noopener noreferrer')
  }
  // javascript: 프로토콜 제거
  if (node.hasAttribute('href')) {
    const href = node.getAttribute('href') ?? ''
    if (href.startsWith('javascript:')) {
      node.removeAttribute('href')
    }
  }
})
```

#### 서버 사이드용 (Node.js 환경)

```typescript
// 서버 사이드에서는 isomorphic-dompurify 사용
import DOMPurify from 'isomorphic-dompurify'

// 사용법은 클라이언트와 동일
const clean = DOMPurify.sanitize(dirtyInput)
```

---

## 2. 안전한 토큰 관리 (Authentication)

### 왜 중요한가

JWT나 세션 토큰을 `localStorage`에 저장하는 것은 XSS 공격 시 토큰이 탈취될 수 있어 지양해야 합니다. 토큰 저장 위치, 만료/회전, 재인증 UX를 함께 설계하지 않으면 한쪽 약점이 시스템 전체를 뚫리게 만듭니다.

> **일상 비유**: 호텔 카드키를 가방에 그대로 두면(localStorage) 가방을 잃어버릴 때 키도 함께 사라집니다. 객실 비번을 직원이 별도 채널로 잠깐만 보여주듯이(HttpOnly + Silent Refresh), 토큰도 노출 표면을 최소화하고 짧게 가져갑니다.

#### 인증 방식 선택 결정 트리

이 그림은 새 기능을 설계할 때 어떤 인증/세션 전략을 적용할지 단계별로 결정하는 흐름을 보여줍니다.

```mermaid
flowchart TD
  Start[새 인증/세션 설계] --> Q1{사용자 유형}
  Q1 -- 신규 가입 + 보안 우선 --> Pass[Passkeys WebAuthn<br/>피싱 내성 + 비밀 미보관]
  Q1 -- 외부 IdP 연동 --> Q2{쿠키 공유 도메인}
  Q2 -- 동일 도메인 --> Session[HttpOnly Session Cookie<br/>+ Lax SameSite]
  Q2 -- 크로스 도메인/SPA --> OAuth[OAuth 2.0 Authorization Code<br/>+ PKCE + state/nonce]
  OAuth --> Q3{토큰 저장 위치}
  Q3 -- access token --> Mem[메모리 변수<br/>새로고침 시 silent refresh]
  Q3 -- refresh token --> HC[HttpOnly + Secure + SameSite Cookie]
  HC --> Rot[Refresh Token Rotation<br/>재사용 감지 시 전체 폐기]
  Q1 -- 기존 비밀번호 --> Q4{2FA 필요}
  Q4 -- 예 --> TwoFA[TOTP/WebAuthn 보조 인증]
  Q4 -- 아니오 --> Warn[권장: 단계적 Passkeys 전환 + Rate limit]
  Pass --> Backup[백업 인증 수단 등록 강제]
  TwoFA --> Backup
```

#### OAuth 2.0 + PKCE 인증 흐름 (RFC 9700 기준)

```mermaid
sequenceDiagram
  autonumber
  participant U as 사용자
  participant FE as 클라이언트(SPA/Next.js)
  participant AS as Authorization Server
  participant RS as Resource Server (API)

  U->>FE: 로그인 시도
  FE->>FE: code_verifier 생성<br/>code_challenge = SHA256(code_verifier)
  FE->>AS: /authorize?response_type=code&PKCE&state&nonce<br/>redirect_uri=exact
  AS->>U: 로그인/동의 화면
  U->>AS: 자격증명 제출
  AS-->>FE: 302 -> redirect_uri?code&state
  FE->>FE: state 검증 (CSRF 방지)
  FE->>AS: /token (code + code_verifier)
  AS-->>FE: access_token (short-lived)<br/>+ refresh_token (HttpOnly cookie)
  FE->>FE: access_token은 메모리에만 저장
  loop API 호출
    FE->>RS: Authorization: Bearer access_token
    RS-->>FE: 응답 또는 401
    opt 401 만료
      FE->>AS: /refresh (HttpOnly cookie 자동 포함)
      AS-->>FE: 새 access_token + 회전된 refresh_token
    end
  end
  Note over FE,AS: refresh_token rotation - 재사용 감지 시 전체 폐기
```

#### 토큰 저장 위치 비교

```mermaid
flowchart LR
  subgraph Bad[권장하지 않음]
    A1[localStorage<br/>access + refresh]
    A2[일반 cookie<br/>HttpOnly 없음]
  end
  subgraph Good[권장 패턴]
    B1[메모리 변수<br/>access token]
    B2[HttpOnly + Secure + SameSite Cookie<br/>refresh token]
  end
  X[XSS 발생] -. 토큰 탈취 가능 .-> A1
  X -. 일부 탈취 가능 .-> A2
  X -. JS 접근 불가 .-> B2
  X -. 메모리만 영향, 새로고침 시 초기화 .-> B1
```

> **현재 표준 권고**:
>
> - **OAuth/OIDC는 RFC 9700 OAuth 2.0 Security BCP를 production 기준으로 삼습니다.** Implicit/Resource Owner Password Credentials 그랜트는 회피하고, Authorization Code + PKCE, exact redirect URI, `state`/`nonce`, issuer 검증을 기본값으로 둡니다. Refresh Token은 sender-constrained 또는 rotation이 권장됩니다.
> - **JWT는 `RFC 8725(JWT BCP)`** 와 그 개정안(`draft-ietf-oauth-rfc8725bis`)에 따라 `alg: none` 금지, 알고리즘 화이트리스트 검증, `kid`/`iss`/`aud` 명시적 검증, 짧은 만료 시간을 적용합니다.
> - **신규 인증은 Passkeys(WebAuthn)를 우선 후보로 검토**합니다. 단, 계정 복구, 기기 변경, 조직 정책, 접근성, 고객 지원 비용을 위협 모델과 UX 테스트로 함께 검증합니다.

### 2.1 HttpOnly + Secure 쿠키 전략

토큰은 JavaScript에서 접근할 수 없는 **HttpOnly 쿠키**에 저장하여 XSS를 통한 탈취를 막습니다.

| 옵션       | 설명                                 | 권장 값                         |
| :--------- | :----------------------------------- | :------------------------------ |
| `HttpOnly` | JS에서 `document.cookie`로 접근 불가 | `true`                          |
| `Secure`   | HTTPS 연결에서만 전송                | `true`                          |
| `SameSite` | 외부 사이트 요청 시 쿠키 전송 제어   | `Lax` 또는 `Strict`             |
| `Path`     | 쿠키가 전송되는 경로 제한            | `/` 또는 `/api`                 |
| `Max-Age`  | 쿠키 만료 시간(초)                   | Refresh Token용: `604800` (7일) |

```typescript
// 서버 측 — 쿠키 설정 예시 (Express)
import { Response } from 'express'

function setAuthCookies(res: Response, refreshToken: string) {
  res.cookie('refresh_token', refreshToken, {
    httpOnly: true, // JS 접근 차단
    secure: true, // HTTPS만 허용
    sameSite: 'lax', // CSRF 기본 방어
    path: '/api/auth', // 인증 경로에서만 전송
    maxAge: 7 * 24 * 60 * 60 * 1000, // 7일 (밀리초)
  })
}
```

### 2.2 In-memory 토큰 + Silent Refresh 패턴

Access Token은 메모리에 변수로만 유지하고, Refresh Token을 HttpOnly 쿠키에 담아 만료 시마다 자동으로 재발급받는 패턴을 권장합니다.

```typescript
// auth/tokenManager.ts — 인메모리 토큰 관리자
let accessToken: string | null = null

export const tokenManager = {
  // Access Token 가져오기
  getToken(): string | null {
    return accessToken
  },

  // Access Token 설정 (메모리에만 저장, 어디에도 persist 하지 않음)
  setToken(token: string): void {
    accessToken = token
  },

  // 토큰 삭제 (로그아웃 시)
  clearToken(): void {
    accessToken = null
  },
}
```

```typescript
// auth/silentRefresh.ts — Silent Refresh 로직
import axios from 'axios'
import { tokenManager } from './tokenManager'

// Refresh 요청 중복 방지를 위한 Promise 캐싱
let refreshPromise: Promise<string> | null = null

export async function silentRefresh(): Promise<string> {
  // 이미 갱신 요청이 진행 중이면 해당 Promise를 재사용 (경쟁 상태 방지)
  if (refreshPromise) {
    return refreshPromise
  }

  refreshPromise = axios
    .post<{ accessToken: string }>(
      '/api/auth/refresh',
      {},
      { withCredentials: true }, // HttpOnly 쿠키 자동 포함
    )
    .then((res) => {
      const newToken = res.data.accessToken
      tokenManager.setToken(newToken)
      return newToken
    })
    .finally(() => {
      refreshPromise = null // 완료 후 캐시 해제
    })

  return refreshPromise
}
```

```typescript
// auth/axiosInstance.ts — Axios 인터셉터로 자동 토큰 관리
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'
import { tokenManager } from './tokenManager'
import { silentRefresh } from './silentRefresh'

const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  withCredentials: true, // 쿠키 자동 포함
})

// 요청 인터셉터 — 모든 요청에 Access Token 자동 첨부
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  const token = tokenManager.getToken()
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 응답 인터셉터 — 401 에러 시 Silent Refresh 후 재시도
api.interceptors.response.use(
  (response) => response,
  async (error: AxiosError) => {
    const originalRequest = error.config as InternalAxiosRequestConfig & {
      _retry?: boolean
    }

    // 401 에러이고, 아직 재시도하지 않은 요청인 경우
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true // 무한 루프 방지 플래그

      try {
        const newToken = await silentRefresh()
        originalRequest.headers.Authorization = `Bearer ${newToken}`
        return api(originalRequest) // 원래 요청 재시도
      } catch (refreshError) {
        // Refresh Token도 만료된 경우 — 로그아웃 처리
        tokenManager.clearToken()
        window.location.href = '/login'
        return Promise.reject(refreshError)
      }
    }

    return Promise.reject(error)
  },
)

export default api
```

### 2.3 토큰 로테이션 (Refresh Token Rotation)

Refresh Token을 한 번 사용하면 폐기하고 새로운 Refresh Token을 발급하는 전략입니다. 토큰이 탈취되더라도 한 번만 사용 가능하므로 피해를 최소화합니다.

#### Refresh Token 회전 상태도

```mermaid
stateDiagram-v2
  [*] --> Issued: 로그인 성공
  Issued --> Used: /refresh 호출
  Used --> Revoked: 즉시 폐기 표시
  Revoked --> NewIssued: 새 refresh 발급
  NewIssued --> Used: 다음 회전
  Revoked --> Compromised: 이미 폐기된 토큰<br/>재사용 시도
  Compromised --> AllRevoked: 사용자 전체 토큰 폐기<br/>강제 로그아웃 + 알림
  AllRevoked --> [*]
  NewIssued --> Expired: maxAge 초과
  Expired --> [*]
```

```typescript
// 서버 측 — Refresh Token Rotation 구현 예시
async function handleRefresh(req: Request, res: Response) {
  const oldRefreshToken = req.cookies.refresh_token

  // 1. DB에서 Refresh Token 유효성 확인
  const tokenRecord = await db.refreshToken.findUnique({
    where: { token: oldRefreshToken },
  })

  if (!tokenRecord || tokenRecord.revoked) {
    // 이미 사용된 토큰이 다시 사용됨 → 토큰 탈취 의심
    // 해당 사용자의 모든 Refresh Token 폐기 (보안 조치)
    await db.refreshToken.updateMany({
      where: { userId: tokenRecord?.userId },
      data: { revoked: true },
    })
    res.status(401).json({ error: '의심스러운 토큰 재사용 감지' })
    return
  }

  // 2. 기존 Refresh Token 폐기
  await db.refreshToken.update({
    where: { id: tokenRecord.id },
    data: { revoked: true },
  })

  // 3. 새로운 토큰 쌍 발급
  const newAccessToken = generateAccessToken(tokenRecord.userId)
  const newRefreshToken = generateRefreshToken()

  // 4. 새 Refresh Token DB 저장
  await db.refreshToken.create({
    data: {
      token: newRefreshToken,
      userId: tokenRecord.userId,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    },
  })

  // 5. 새 Refresh Token을 HttpOnly 쿠키로 설정
  res.cookie('refresh_token', newRefreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/api/auth',
    maxAge: 7 * 24 * 60 * 60 * 1000,
  })

  res.json({ accessToken: newAccessToken })
}
```

### 2.4 Passkeys / WebAuthn — 비밀번호 없는 인증 (현재 권장)

Passkeys는 `WebAuthn` 표준 위에 구축된 공개키 기반 자격증명으로, **피싱 내성·재사용 불가·서버에 비밀 미보관**의 세 가지 보안 특성을 만족합니다. 현재 공식 문서 기준 W3C `WebAuthn Level 3` 워킹 드래프트가 진행 중이며, 주요 플랫폼(iOS/Android/Windows/macOS)이 클라우드 동기화 패스키를 기본 지원합니다.

```typescript
// 등록(Registration): 서버가 challenge를 만들어 클라이언트에 전달
export async function registerPasskey(username: string) {
  // 1) 서버에서 등록 옵션 요청
  const options = await api.get('/auth/passkey/register-options', { params: { username } })

  // 2) 브라우저가 사용자 인증기(생체, 보안키)로 키쌍 생성
  const credential = await navigator.credentials.create({
    publicKey: {
      ...options,
      // 서버 challenge를 ArrayBuffer로 변환 (base64url 디코드)
      challenge: base64UrlToBuffer(options.challenge),
      user: {
        ...options.user,
        id: base64UrlToBuffer(options.user.id),
      },
    },
  })

  // 3) 공개키와 attestation을 서버로 전송 → DB 저장
  await api.post('/auth/passkey/register', serializeCredential(credential))
}

// 인증(Authentication): 서버 challenge에 사용자 비밀키로 서명
export async function loginWithPasskey() {
  const options = await api.get('/auth/passkey/login-options')

  const assertion = await navigator.credentials.get({
    publicKey: {
      ...options,
      challenge: base64UrlToBuffer(options.challenge),
    },
    // 자동 입력(autofill)을 위해 conditional mediation 사용
    mediation: 'conditional',
  })

  // 서버는 저장된 공개키로 서명 검증 후 세션 발급
  return api.post('/auth/passkey/verify', serializeAssertion(assertion))
}
```

> **운영 체크포인트**:
>
> - **백업 인증 수단 필수**: 디바이스 분실 대비 두 번째 패스키 등록을 가입 플로우에 포함하세요. 매직 링크/SMS OTP는 폴백으로만 사용.
> - **사용자 ID(`user.id`)는 안정적인 식별자**여야 합니다. 이메일을 직접 사용하면 변경 시 패스키가 무효화됩니다. 내부 UUID 사용 권장.
> - **RP ID** 는 eTLD+1(예: `example.com`)을 사용해 서브도메인 간 공유 가능하게 설계합니다.
> - **서버 라이브러리**: Node는 `@simplewebauthn/server`, Python은 `webauthn`, Go는 `go-webauthn` 같은 대표 후보를 검토합니다.

---

## 3. CSRF (Cross-Site Request Forgery) 방어

### 왜 중요한가

브라우저가 쿠키를 자동으로 전송하는 특성을 악용한 공격을 막습니다. 사용자가 의도하지 않은 요청이 인증된 상태로 서버에 도달하는 것을 방지해야 합니다.

> **일상 비유**: 누군가 내 휴대폰을 잠금 해제된 상태에서 손에 쥐여 주고 "결제 버튼 누르세요"라고 시키는 것과 같습니다. 카드(쿠키)는 이미 내 폰(브라우저) 안에 있으므로, 시키는 대로 누르면 자동 결제됩니다. CSRF 토큰은 "이 결제가 정말 내 의지인가"를 확인하는 추가 도장입니다.

#### CSRF Synchronizer Token 흐름

```mermaid
sequenceDiagram
  autonumber
  participant U as 사용자(피해자)
  participant App as 정상 사이트(app.com)
  participant Att as 공격자 페이지

  U->>App: 로그인 (세션 쿠키 발급)
  App->>U: 페이지 렌더 + CSRF 토큰 (메타 또는 쿠키)
  U->>App: 정상 POST + X-CSRF-Token 헤더
  App-->>U: 토큰 일치 -> 처리

  Note over U,Att: 공격 시도
  U->>Att: 공격자가 만든 페이지 방문
  Att->>App: <form action=/transfer> 자동 제출
  App-->>Att: 쿠키는 자동 포함되지만<br/>X-CSRF-Token 없음 -> 403 거부
```

### 3.1 SameSite 쿠키 설정

Chrome 80(2020년 2월)부터 모든 Chromium 기반 브라우저는 **`SameSite` 속성이 없는 쿠키를 `Lax`로 기본 해석**합니다. SameSite를 생략해도 1차 컨텍스트에서만 전송되지만, 보안 표준에서는 **명시적으로 선언해 의도를 코드로 드러냅니다**.

| 값       | 동작                                              | 적합한 경우                                           |
| :------- | :------------------------------------------------ | :---------------------------------------------------- |
| `Strict` | 외부 사이트에서 오는 모든 요청에 쿠키 미전송      | 은행, 결제 등 높은 보안 필요                          |
| `Lax`    | GET 네비게이션은 허용, POST 등은 차단             | 일반적인 웹 서비스, 현대 브라우저 기본 정책과 잘 맞음 |
| `None`   | 모든 요청에 쿠키 전송 (`Secure` 필수, HTTPS 한정) | 서드파티 임베드·결제 위젯 등 명확한 사유              |

> **최신 운영 주의사항**: `SameSite=None` 쿠키는 **`Secure` 필수 + HTTPS 한정**으로만 동작합니다. 또한 Chrome의 서드파티 쿠키 단계적 제한 정책에 따라, SaaS 임베드·외부 결제 위젯은 `Storage Access API` 또는 `Cross-Origin-Opener-Policy: restrict-properties` 같은 대안 통합을 미리 준비하세요.

### 3.2 CSRF 토큰 패턴 (Synchronizer Token)

서버가 생성한 고유 토큰을 클라이언트가 요청에 포함하여 전송하는 전통적인 방어법입니다.

```typescript
// 서버 측 — CSRF 토큰 생성 및 검증 (Express 미들웨어)
import crypto from 'crypto'
import { Request, Response, NextFunction } from 'express'

// CSRF 토큰 생성 엔드포인트
app.get('/api/csrf-token', (req: Request, res: Response) => {
  // 세션마다 고유한 CSRF 토큰 생성
  const csrfToken = crypto.randomBytes(32).toString('hex')

  // 세션에 저장 (서버 측 검증용)
  req.session.csrfToken = csrfToken

  // 클라이언트에 토큰 전달
  res.json({ csrfToken })
})

// CSRF 검증 미들웨어
function verifyCsrf(req: Request, res: Response, next: NextFunction) {
  // GET, HEAD, OPTIONS는 검증 생략 (안전한 메서드)
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
    return next()
  }

  const clientToken = req.headers['x-csrf-token'] as string
  const serverToken = req.session.csrfToken

  if (!clientToken || clientToken !== serverToken) {
    res.status(403).json({ error: 'CSRF 토큰이 유효하지 않습니다.' })
    return
  }

  next()
}

// 모든 API 라우트에 적용
app.use('/api', verifyCsrf)
```

```typescript
// 클라이언트 측 — CSRF 토큰을 Axios에 자동 포함
import axios from 'axios'

let csrfToken: string | null = null

// 앱 초기화 시 CSRF 토큰 요청
export async function initCsrfToken(): Promise<void> {
  const res = await axios.get<{ csrfToken: string }>('/api/csrf-token', {
    withCredentials: true,
  })
  csrfToken = res.data.csrfToken
}

// Axios 인터셉터로 모든 상태 변경 요청에 CSRF 토큰 포함
axios.interceptors.request.use((config) => {
  if (csrfToken && config.method !== 'get') {
    config.headers['X-CSRF-Token'] = csrfToken
  }
  return config
})
```

### 3.3 Double Submit Cookie 패턴

서버 세션 없이 CSRF를 방어하는 경량 패턴입니다. 쿠키와 헤더에 동일한 값을 담아 비교합니다.

```typescript
// 서버 측 — Double Submit Cookie 설정
import crypto from 'crypto'

app.get('/api/auth/login', (req, res) => {
  // ... 인증 로직 ...

  // CSRF 방어용 토큰을 일반 쿠키(JS 접근 가능)로 설정
  const csrfToken = crypto.randomBytes(32).toString('hex')
  res.cookie('csrf_token', csrfToken, {
    httpOnly: false, // JS에서 읽을 수 있어야 함
    secure: true,
    sameSite: 'lax',
    path: '/',
  })

  res.json({ success: true })
})

// 검증 미들웨어
function verifyDoubleSubmit(req: Request, res: Response, next: NextFunction) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
    return next()
  }

  // 쿠키의 값과 헤더의 값이 일치하는지 확인
  const cookieToken = req.cookies.csrf_token
  const headerToken = req.headers['x-csrf-token']

  if (!cookieToken || cookieToken !== headerToken) {
    res.status(403).json({ error: 'CSRF 검증 실패' })
    return
  }

  next()
}
```

```typescript
// 클라이언트 측 — 쿠키에서 읽어 헤더로 전송
function getCookie(name: string): string | null {
  const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`))
  return match ? match[2] : null
}

// Axios 인터셉터
axios.interceptors.request.use((config) => {
  if (config.method !== 'get') {
    const csrfToken = getCookie('csrf_token')
    if (csrfToken) {
      config.headers['X-CSRF-Token'] = csrfToken
    }
  }
  return config
})
```

### 3.4 Custom Header 방어

API 요청 시 커스텀 헤더를 포함하면 브라우저의 CORS preflight에 의해 다른 사이트에서의 요청이 차단됩니다.

> **일상 비유**: 행사 입장 시 손목띠(SameSite)만 보지 않고, 도장(CSRF 토큰), 추가 신분증(Custom Header), 본인 확인서(Re-auth)까지 다층으로 확인하는 방식입니다.

#### CSRF 방어 다층 결정 트리

이 그림은 신규 API/액션을 추가할 때 어떤 CSRF 방어 조합을 적용할지 결정하는 흐름을 보여줍니다.

```mermaid
flowchart TD
  Start[신규 mutation 엔드포인트] --> Q1{인증 방식}
  Q1 -- 쿠키 기반 세션 --> Q2{메서드}
  Q2 -- GET/HEAD --> Safe[CSRF 위험 낮음<br/>SameSite=Lax로 충분]
  Q2 -- POST/PUT/DELETE --> Q3{교차 출처 임베드 가능}
  Q3 -- 없음 --> Lax[SameSite=Strict<br/>+ Custom Header 검사]
  Q3 -- 있음 결제/위젯 --> Token[Synchronizer Token<br/>또는 Double Submit Cookie]
  Token --> Q4{민감도}
  Q4 -- 최고 자금이체/관리 --> Reauth[Token + Step-up Re-auth<br/>WebAuthn 재인증]
  Q4 -- 일반 --> Token2[Token + Custom Header 조합]
  Q1 -- Bearer 토큰 SPA --> Bearer[CSRF 본질적 영향 낮음<br/>단 Storage Access API 대비]
  Q1 -- Passkey 인증 --> Pass[리플레이 방지<br/>CSRF 위험 자체 낮음]
```

```typescript
// 모든 API 요청에 커스텀 헤더 추가
const api = axios.create({
  baseURL: '/api',
  headers: {
    'X-Requested-With': 'XMLHttpRequest', // 커스텀 헤더 — preflight 트리거
  },
})

// 서버 측 — 커스텀 헤더 검증
function verifyCustomHeader(req: Request, res: Response, next: NextFunction) {
  if (req.headers['x-requested-with'] !== 'XMLHttpRequest') {
    res.status(403).json({ error: '잘못된 요청입니다.' })
    return
  }
  next()
}
```

---

## 4. 보안 점검 자동화 (CI/CD 연계)

보안은 사람이 매번 체크하기 어렵습니다. CI/CD 파이프라인에 자동화 도구를 통합하세요.

### 4.1 npm audit — CI 파이프라인 통합

```yaml
# .ci/workflows/security-audit.yml
name: Security Audit

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  # 매주 월요일 오전 9시(KST) 정기 스캔
  schedule:
    - cron: '0 0 * * 1'

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Node.js 설정
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: 의존성 설치
        run: npm ci

      - name: npm audit 실행 (high/critical만 실패 처리)
        run: npm audit --audit-level=high

      - name: 라이선스 검사
        run: npx license-checker --failOn "GPL-3.0;AGPL-3.0"

      - name: 번들 크기 확인 (보안 외 품질 게이트)
        run: npx size-limit
```

### 4.2 Snyk 연동

```yaml
# .ci/workflows/snyk.yml
name: Snyk Security Scan

on:
  pull_request:
    branches: [main]

jobs:
  snyk:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Snyk 의존성 취약점 스캔
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high

      - name: Snyk 코드 취약점 스캔
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          command: code test
```

```json
// .snyk — Snyk 정책 파일
{
  "version": "v1.25.0",
  "ignore": {},
  "patch": {},
  "language-settings": {
    "javascript": {
      "severity-threshold": "high"
    }
  }
}
```

### 4.3 보안 헤더 자동 검증

```typescript
// scripts/check-security-headers.ts — 배포 후 보안 헤더 검증 스크립트
const REQUIRED_HEADERS: Record<string, string | RegExp> = {
  'content-security-policy': /default-src/,
  'x-content-type-options': 'nosniff',
  'x-frame-options': /DENY|SAMEORIGIN/,
  'strict-transport-security': /max-age=\d+/,
  'referrer-policy': /strict-origin|no-referrer/,
  'permissions-policy': /.+/,
}

async function checkSecurityHeaders(url: string): Promise<void> {
  const response = await fetch(url, { method: 'HEAD' })
  const failures: string[] = []

  for (const [header, expected] of Object.entries(REQUIRED_HEADERS)) {
    const value = response.headers.get(header)

    if (!value) {
      failures.push(`❌ [누락] ${header}`)
      continue
    }

    if (typeof expected === 'string' && value !== expected) {
      failures.push(`❌ [불일치] ${header}: "${value}" (예상: "${expected}")`)
    } else if (expected instanceof RegExp && !expected.test(value)) {
      failures.push(`❌ [불일치] ${header}: "${value}" (패턴: ${expected})`)
    } else {
      console.log(`✅ ${header}: ${value}`)
    }
  }

  if (failures.length > 0) {
    console.error('\n보안 헤더 검증 실패:')
    failures.forEach((f) => console.error(f))
    process.exit(1)
  }

  console.log('\n✅ 모든 보안 헤더가 올바르게 설정되어 있습니다.')
}

// 사용: npx ts-node scripts/check-security-headers.ts https://my-app.com
const targetUrl = process.argv[2]
if (!targetUrl) {
  console.error('사용법: npx ts-node scripts/check-security-headers.ts <URL>')
  process.exit(1)
}
checkSecurityHeaders(targetUrl)
```

```yaml
# CI에서 배포 후 자동 검증
- name: 보안 헤더 검증
  run: npx ts-node scripts/check-security-headers.ts ${{ env.DEPLOY_URL }}
```

---

## 5. CORS 정책 이해와 설정

### 왜 중요한가

CORS(Cross-Origin Resource Sharing)는 다른 출처(origin)에서의 리소스 요청을 제어하는 브라우저 보안 메커니즘입니다. 잘못된 CORS는 자격증명 탈취, 데이터 노출로 이어지고, 너무 강한 CORS는 정상 서비스 통합을 막습니다.

> **일상 비유**: 본사 출입증으로 협력사 빌딩에 들어가려면 사전에 협력사 보안실(서버)이 본사를 화이트리스트에 올려야 하고, 본인은 사전 등록(Preflight)을 거쳐야 합니다.

### 5.1 CORS 동작 원리

```
1. 브라우저가 다른 출처로 요청 전송
2. Simple Request가 아니면 → Preflight (OPTIONS) 요청 발생
3. 서버가 Access-Control-Allow-* 헤더로 허용 여부 응답
4. 허용된 경우에만 실제 요청 전송
```

#### CORS Preflight 흐름

```mermaid
sequenceDiagram
  participant B as 브라우저
  participant S as 서버

  Note over B,S: Simple Request 조건 미충족 시
  B->>S: OPTIONS /api/data<br/>Origin, Access-Control-Request-Method, Headers
  S-->>B: 200 OK<br/>Access-Control-Allow-Origin: https://app.com<br/>Allow-Methods, Allow-Headers, Allow-Credentials<br/>Max-Age: 86400
  alt 정책 일치
    B->>S: 실제 요청 (POST/PUT...)<br/>Authorization, X-CSRF-Token 등
    S-->>B: 정상 응답
  else 정책 불일치
    B->>B: 차단, 콘솔 CORS 에러
  end
  Note over B: Max-Age 동안 동일 prefligh 캐시 -> OPTIONS 생략
```

**Simple Request 조건** (Preflight 생략):

- 메서드: GET, HEAD, POST
- 헤더: Accept, Content-Type(일부), Accept-Language, Content-Language
- Content-Type: `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`

### 5.2 서버 측 CORS 설정 (Express)

```typescript
import cors from 'cors'

// ❌ 나쁜 예 — 모든 출처 허용 (개발 환경에서만)
app.use(cors({ origin: '*' }))

// ✅ 좋은 예 — 허용할 출처를 명시적으로 지정
const allowedOrigins = [
  'https://my-app.com',
  'https://admin.my-app.com',
  // 개발 환경
  ...(process.env.NODE_ENV === 'development'
    ? ['http://localhost:3000', 'http://localhost:3001']
    : []),
]

app.use(
  cors({
    origin: (origin, callback) => {
      // origin이 없는 경우 (같은 출처 또는 서버 간 요청)
      if (!origin || allowedOrigins.includes(origin)) {
        callback(null, true)
      } else {
        callback(new Error(`CORS 정책에 의해 차단됨: ${origin}`))
      }
    },
    credentials: true, // 쿠키 포함 허용 (withCredentials)
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
    allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token', 'X-Requested-With'],
    exposedHeaders: ['X-Total-Count'], // 클라이언트에서 접근 가능한 응답 헤더
    maxAge: 86400, // Preflight 캐시: 24시간 (OPTIONS 요청 최소화)
  }),
)
```

### 5.3 Next.js CORS 설정

```typescript
// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          { key: 'Access-Control-Allow-Origin', value: 'https://my-app.com' },
          { key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE,PATCH' },
          { key: 'Access-Control-Allow-Headers', value: 'Content-Type,Authorization,X-CSRF-Token' },
          { key: 'Access-Control-Allow-Credentials', value: 'true' },
          { key: 'Access-Control-Max-Age', value: '86400' },
        ],
      },
    ]
  },
}

export default nextConfig
```

### 5.4 클라이언트 측 주의사항

```typescript
// credentials를 사용할 때 주의사항
const api = axios.create({
  baseURL: 'https://api.my-app.com',
  withCredentials: true, // 쿠키를 포함해야 할 때만 true
})

// ⚠️ withCredentials: true일 때 서버의 Access-Control-Allow-Origin은
//    "*"가 될 수 없음. 반드시 구체적인 출처를 명시해야 함.
```

---

## 6. 의존성 보안: Supply Chain Attack 방어 (OWASP A03:2025)

### 왜 중요한가

npm 패키지를 통한 공급망 공격(Supply Chain Attack)이 증가하고 있으며, **OWASP Top 10:2025의 A03(Software Supply Chain Failures)** 로 별도 카테고리화되었습니다. Lockfile 무결성, 패키지 출처 검증, 설치 스크립트 제어, **빌드 시스템·CI 토큰 보호**가 핵심입니다.

> **일상 비유**: 식자재 유통은 농장 → 도매 → 식당까지 단계마다 송장과 봉인이 있어야 위·변조가 즉시 드러납니다. 코드도 마찬가지로 source → registry → build artifact → 배포까지 단계마다 무결성 증거가 있어야 합니다.

#### 공급망 검증 파이프라인

```mermaid
flowchart LR
  subgraph Source[소스]
    A1[git commit signed]
    A2[CODEOWNERS / branch protection]
  end
  subgraph Build[빌드]
    B1[npm ci<br/>lockfile 기반]
    B2[npm audit signatures]
    B3[npm audit --audit-level=high]
    B4[ignore-scripts / 화이트리스트]
  end
  subgraph Artifact[산출물]
    C1[SBOM 생성]
    C2[build provenance / attestation]
    C3[checksum]
  end
  subgraph Release[배포]
    D1[gh attestation verify]
    D2[registry signing key check]
    D3[Pin by digest, 환경별 promote]
  end
  A1 --> B1
  A2 --> B1
  B1 --> B2 --> B3 --> B4
  B4 --> C1 --> C2 --> C3
  C3 --> D1 --> D2 --> D3
  D3 -. 사고 시 .-> R[Rollback by checksum<br/>+ alert]
```

### 6.1 Lockfile 무결성 보장

```bash
# CI에서는 반드시 npm ci 사용 (lockfile 기반 정확한 설치)
npm ci          # ✅ package-lock.json과 정확히 일치하는 버전 설치
npm install     # ❌ CI에서 사용 금지 — lockfile이 변경될 수 있음
```

```json
// .npmrc — 보안 강화 설정
{
  "// lockfile 무결성": "",
  "package-lock": true,
  "// 설치 스크립트 제어 (typosquatting 방어)": "",
  "ignore-scripts": true,
  "// 정확한 버전 저장": "",
  "save-exact": true
}
```

```ini
# .npmrc 파일 형식
package-lock=true
save-exact=true
ignore-scripts=true
```

> **주의**: `ignore-scripts=true`를 설정하면 `postinstall` 스크립트가 실행되지 않습니다. 일부 패키지(esbuild, sharp 등)는 postinstall이 필요하므로, 해당 패키지는 별도로 허용합니다.

### 6.2 provenance와 attestation (출처 검증)

패키지 매니저의 서명 검증은 dependency 수신 단계의 증거이고, artifact attestation은 빌드 산출물이 어떤 source/workflow에서 만들어졌는지 증명하는 배포 단계의 증거입니다. production 승격 기준은 둘을 분리해서 봅니다.

```bash
# 패키지 서명/출처 확인
npm audit signatures

# 특정 패키지의 출처 상세 확인
npm view <패키지명> --json | jq '.dist.attestations'

# 빌드 산출물 attestation 검증 예시
gh attestation verify dist/app.tar.gz -R ORG/REPO
```

| 증거                         | 막는 위험                                      | 보관 위치                      |
| :--------------------------- | :--------------------------------------------- | :----------------------------- |
| lockfile diff                | 의도하지 않은 dependency drift                 | PR                             |
| package signature/provenance | registry 수신 단계 변조                        | CI report                      |
| SBOM                         | 어떤 component가 artifact에 포함됐는지 추적    | release artifact               |
| build provenance/attestation | source revision, workflow, builder 위변조 확인 | release artifact 또는 registry |
| checksum                     | 다운로드/업로드 중 artifact 변조 확인          | release manifest               |

### 6.3 의존성 보안 CI 파이프라인

```yaml
# .ci/workflows/supply-chain.yml
name: Supply Chain Security

on:
  pull_request:
    paths:
      - 'package.json'
      - 'package-lock.json'

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Node.js 설정
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: lockfile 변경 감지 경고
        run: |
          # package-lock.json의 변경이 의도적인지 확인
          echo "📦 lockfile이 변경되었습니다. 의존성 변경을 리뷰해주세요."
          git diff origin/main -- package-lock.json | head -100

      - name: 의존성 설치 (lockfile 기반)
        run: npm ci

      - name: npm audit 서명 검증
        run: npm audit signatures

      - name: npm audit 취약점 검사
        run: npm audit --audit-level=high

      - name: 새로 추가된 의존성 목록 출력
        run: |
          # 새로 추가된 패키지 확인
          git diff origin/main -- package.json | grep '^\+.*":'  | grep -v '^\+\+\+' || echo "새로운 의존성 없음"
```

### 6.4 안전한 의존성 관리 원칙

> **일상 비유**: 새 식자재를 들이듯, 새 패키지도 출처(농장) → 유통(registry) → 사용 빈도(소진) → 보관(버전 고정)을 모두 확인합니다.

#### 새 의존성 도입 결정 트리

이 그림은 PR에서 신규 npm 패키지가 추가될 때 머지 가능성 판단의 게이트를 보여줍니다.

```mermaid
flowchart TD
  Start[신규 의존성 추가 PR] --> Q1{대안: shared/lib<br/>자체 구현 가능}
  Q1 -- 가능 --> Self[자체 구현 권장<br/>10~20줄 이내]
  Q1 -- 불가 --> Q2{npm audit signatures 통과}
  Q2 -- 실패 --> Reject[즉시 반려<br/>signing key 검증 불가]
  Q2 -- 통과 --> Q3{최근 6개월 활성 + GitHub stars}
  Q3 -- 아니오 --> Slow[느린 검토 -- ADR 작성]
  Q3 -- 예 --> Q4{보안 advisory 이력}
  Q4 -- High/Critical 있음 --> Q5{대안 패키지 존재}
  Q5 -- 예 --> Switch[대안 도입]
  Q5 -- 아니오 --> Risk[ADR + 책임자 지정 + 모니터링]
  Q4 -- 깨끗 --> Q6{ignore-scripts 호환}
  Q6 -- 예 --> Lock[정확한 버전 고정 + SBOM 추가]
  Q6 -- 아니오 --> Allow[postinstall 화이트리스트<br/>+ 빌드 격리]
  Lock --> Done[머지 가능]
  Allow --> Done
```

```typescript
// ❌ 위험 — 범위 지정자 사용
"dependencies": {
  "lodash": "^4.17.0",     // ^ → 4.x.x 범위의 최신 버전 설치 가능
  "moment": "~2.29.0"      // ~ → 2.29.x 범위의 최신 버전 설치 가능
}

// ✅ 안전 — 정확한 버전 고정 (save-exact 옵션 활용)
"dependencies": {
  "lodash": "4.17.21",     // 정확한 버전만 설치
  "dayjs": "1.11.10"
}
```

---

## 7. 환경 변수 및 시크릿 관리

프론트엔드에서 환경 변수를 잘못 사용하면 민감한 정보가 클라이언트에 노출될 수 있습니다.

> **일상 비유**: 가방 안에 "공개해도 되는 명함"과 "절대 보여주면 안 되는 인감"을 같이 들고 다니면 사고가 납니다. 환경 변수도 두 칸으로 분리하고, 어느 칸이 어디로 가는지 분류해야 합니다.

#### 환경 변수 분류 결정 트리

이 그림은 새 환경 변수를 추가할 때 NEXT*PUBLIC* 접두사 사용 여부를 단계적으로 판단하는 흐름을 보여줍니다.

```mermaid
flowchart TD
  Start[새 환경 변수 추가] --> Q1{값의 민감도}
  Q1 -- 비밀번호/토큰/시크릿 --> Server[서버 전용<br/>NEXT_PUBLIC_ 금지<br/>접두사 없이 보관]
  Q1 -- 공개 가능 --> Q2{사용 위치}
  Q2 -- 클라이언트 코드에서 참조 --> Public[NEXT_PUBLIC_ 접두사<br/>예 NEXT_PUBLIC_API_URL]
  Q2 -- 서버 라우트/Action에서만 --> Server
  Server --> Q3{시크릿 회전 주기}
  Q3 -- 90일 이상 정적 --> SM[Secret Manager<br/>예 cloud KMS / Vault]
  Q3 -- 자주 회전 --> Vault[CI Secret 또는 런타임 주입]
  Public --> Q4{도메인 노출 안전한가}
  Q4 -- 예 --> Ok[빌드 시 인라인]
  Q4 -- 아니오 --> Reclass[Server 전용으로 재분류]
  Server --> Scan[빌드 산출물 시크릿 스캔 필수]
```

### 7.1 NEXT*PUBLIC* 접두사의 위험성

```bash
# .env.local

# ✅ 클라이언트에 노출되어도 괜찮은 값 — NEXT_PUBLIC_ 접두사 사용
NEXT_PUBLIC_API_URL=https://api.my-app.com
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_OBSERVABILITY_DSN=https://example.invalid/project

# ❌ 절대로 NEXT_PUBLIC_을 붙이면 안 되는 값
# NEXT_PUBLIC_DB_PASSWORD=...       → 브라우저에서 볼 수 있음!
# NEXT_PUBLIC_JWT_SECRET=...        → 브라우저에서 볼 수 있음!
# NEXT_PUBLIC_CLOUD_SECRET_KEY=...    → 브라우저에서 볼 수 있음!

# ✅ 서버 사이드에서만 사용하는 값 — 접두사 없이 설정
DATABASE_URL=postgresql://user:pass@host:5432/db
JWT_SECRET=super-secret-key-never-expose
CLOUD_SECRET_ACCESS_KEY=xxxxxxxxxxxx
STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxx
```

### 7.2 환경 변수 검증 (Zod 활용)

```typescript
// env.ts — 환경 변수 타입 안전성 확보
import { z } from 'zod'

// 서버 전용 환경 변수 스키마
const serverEnvSchema = z.object({
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  CLOUD_SECRET_ACCESS_KEY: z.string().min(1),
  NODE_ENV: z.enum(['development', 'production', 'test']),
})

// 클라이언트 환경 변수 스키마
const clientEnvSchema = z.object({
  NEXT_PUBLIC_API_URL: z.string().url(),
  NEXT_PUBLIC_GA_ID: z.string().optional(),
})

// 서버 환경 변수 검증 (빌드 시 실패하도록)
export const serverEnv = serverEnvSchema.parse(process.env)

// 클라이언트 환경 변수 검증
export const clientEnv = clientEnvSchema.parse({
  NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  NEXT_PUBLIC_GA_ID: process.env.NEXT_PUBLIC_GA_ID,
})
```

### 7.3 .env 파일 관리 원칙

```gitignore
# .gitignore — 환경 변수 파일은 반드시 Git에서 제외
.env
.env.local
.env.production
.env.*.local

# .env.example만 커밋 (값 없이 키만 포함)
!.env.example
```

```bash
# .env.example — 팀원 온보딩용 (실제 값은 절대 포함하지 않음)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=your-jwt-secret-min-32-chars-here
NEXT_PUBLIC_API_URL=http://localhost:8080
```

```typescript
// 시크릿이 클라이언트 번들에 포함되었는지 검사하는 스크립트
// scripts/check-secrets-in-bundle.ts
import fs from 'fs'
import path from 'path'

const FORBIDDEN_PATTERNS = [
  /sk_live_[a-zA-Z0-9]+/, // Stripe 시크릿 키
  /AKIA[A-Z0-9]{16}/, // cloud provider access key
  /-----BEGIN (RSA )?PRIVATE KEY-----/, // 개인 키
  /postgresql:\/\/[^"'\s]+/, // DB 연결 문자열
]

function scanBundle(dir: string): void {
  const files = fs.readdirSync(dir, { recursive: true, encoding: 'utf-8' })
  const jsFiles = files.filter((f) => f.endsWith('.js') || f.endsWith('.js.map'))

  for (const file of jsFiles) {
    const content = fs.readFileSync(path.join(dir, file), 'utf-8')
    for (const pattern of FORBIDDEN_PATTERNS) {
      if (pattern.test(content)) {
        console.error(`🚨 번들에서 시크릿 감지: ${file} (패턴: ${pattern})`)
        process.exit(1)
      }
    }
  }
  console.log('✅ 번들에서 시크릿이 감지되지 않았습니다.')
}

scanBundle('.next/static')
```

---

## 8. 보안 헤더 운영 가이드

HTTP 응답 헤더를 통해 브라우저의 보안 동작을 제어할 수 있습니다.

> **일상 비유**: 빌딩 출입 보안과 같습니다. 정문 단속(HSTS), 출입증 검사(CSP), 엘리베이터 접근 제어(Permissions-Policy), 외부인 동반 금지(Frame-Ancestors) — 각 헤더가 서로 보완해야 안전합니다.

#### 보안 헤더 의존 관계 다이어그램

이 그림은 주요 보안 헤더들이 어떤 위협을 막고 서로 어떻게 보완하는지 보여줍니다.

```mermaid
flowchart TD
  Req[HTTP 요청] --> H1[HSTS<br/>HTTPS 강제]
  H1 --> H2[Content-Security-Policy<br/>스크립트/스타일 출처 제한]
  H2 --> H2a[script-src nonce]
  H2 --> H2b[require-trusted-types-for]
  H2 --> H2c[frame-ancestors<br/>X-Frame-Options 대체]
  H2 --> H2d[form-action]
  H1 --> H3[X-Content-Type-Options nosniff<br/>MIME 스니핑 차단]
  H1 --> H4[Referrer-Policy<br/>외부로 누출되는 정보 제한]
  H1 --> H5[Permissions-Policy<br/>카메라/마이크/결제 등 차단]
  H1 --> H6[Cross-Origin-Opener-Policy<br/>+ Embedder + Resource Policy]
  H6 --> Iso[Cross-Origin Isolation<br/>SharedArrayBuffer/precise timer 사용]
  H2c --> CJ[Clickjacking 방어]
  H2 --> XSS[DOM/Reflected XSS 차단]
  H3 --> MIME[MIME confusion 공격 차단]
  H6 --> Spectre[Spectre/사이드 채널 완화]
```

### 8.1 필수 보안 헤더 일람

| 헤더                           | 설명                                                                    | 권장 값                                                                 |
| :----------------------------- | :---------------------------------------------------------------------- | :---------------------------------------------------------------------- |
| `Strict-Transport-Security`    | HTTPS 강제 (HSTS)                                                       | `max-age=31536000; includeSubDomains; preload`                          |
| `Content-Security-Policy`      | 스크립트·리소스 출처 제어                                               | `default-src 'self'; ...` + `require-trusted-types-for 'script'` (CSP3) |
| `X-Content-Type-Options`       | MIME 타입 스니핑 방지                                                   | `nosniff`                                                               |
| `X-Frame-Options`              | 클릭재킹 방지 (iframe 삽입 차단). **CSP `frame-ancestors`로 대체 권장** | `DENY` 또는 `SAMEORIGIN`                                                |
| `Referrer-Policy`              | Referer 헤더 정보 제한                                                  | `strict-origin-when-cross-origin`                                       |
| `Permissions-Policy`           | 브라우저 기능(카메라, 마이크 등) 제어                                   | 아래 예시 참고                                                          |
| `X-DNS-Prefetch-Control`       | DNS 프리페치 제어                                                       | `off` (개인정보 보호)                                                   |
| `Cross-Origin-Opener-Policy`   | 팝업과 오프너 간 격리                                                   | `same-origin` (결제·OAuth 팝업이 있다면 `restrict-properties`)          |
| `Cross-Origin-Embedder-Policy` | 리소스 임베드 정책                                                      | `require-corp` (또는 `credentialless`)                                  |
| `Cross-Origin-Resource-Policy` | 자원이 다른 오리진에서 임베드되는 것 차단                               | `same-origin` 또는 `same-site`                                          |

### 8.2 Next.js 보안 헤더 설정

```typescript
// next.config.ts
import type { NextConfig } from 'next'

const securityHeaders = [
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=31536000; includeSubDomains; preload',
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff',
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY',
  },
  {
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin',
  },
  {
    key: 'Permissions-Policy',
    // 사용하지 않는 브라우저 기능을 명시적으로 차단
    value: [
      'camera=()', // 카메라 사용 금지
      'microphone=()', // 마이크 사용 금지
      'geolocation=()', // 위치 정보 사용 금지
      'payment=()', // 결제 API 사용 금지
      'usb=()', // USB 접근 금지
      'interest-cohort=()', // 브라우저 광고 추적 실험 거부
    ].join(', '),
  },
  {
    key: 'X-DNS-Prefetch-Control',
    value: 'off',
  },
  {
    key: 'Cross-Origin-Opener-Policy',
    // 결제·OAuth 등 서드파티 팝업을 사용한다면 "restrict-properties"로 완화
    // (cross-origin isolation은 유지하면서 외부 팝업과의 제한적 상호작용 허용)
    value: 'same-origin',
  },
  {
    key: 'Cross-Origin-Resource-Policy',
    value: 'same-origin',
  },
]

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        // 모든 경로에 보안 헤더 적용
        source: '/(.*)',
        headers: securityHeaders,
      },
    ]
  },
  // X-Powered-By 헤더 제거 (서버 정보 노출 방지)
  poweredByHeader: false,
}

export default nextConfig
```

### 8.3 각 헤더 상세 설명

#### Strict-Transport-Security (HSTS)

브라우저에 "이 사이트는 항상 HTTPS로만 접속하라"고 지시합니다. 중간자 공격(MITM)과 SSL 스트리핑 공격을 방어합니다.

```
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
```

- `max-age=31536000`: 1년간 HTTPS 강제
- `includeSubDomains`: 모든 서브도메인에도 적용
- `preload`: 브라우저의 HSTS preload 목록에 등록 (hstspreload.org)

#### Referrer-Policy

다른 사이트로 이동할 때 Referer 헤더에 포함되는 정보를 제어합니다.

| 값                                | 동작                                                                    |
| :-------------------------------- | :---------------------------------------------------------------------- |
| `no-referrer`                     | Referer 헤더를 전송하지 않음                                            |
| `origin`                          | 출처(origin)만 전송                                                     |
| `strict-origin-when-cross-origin` | 같은 출처: 전체 URL, 다른 출처: origin만, HTTPS→HTTP: 미전송 **(권장)** |

#### Permissions-Policy

브라우저 기능에 대한 접근을 세밀하게 제어합니다.

```
Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=()
```

- `()`: 완전히 차단
- `(self)`: 같은 출처에서만 허용
- `(self "https://trusted.com")`: 같은 출처와 특정 도메인만 허용
- `*`: 모든 출처 허용 (비권장)

---

## 9. 주의사항 및 흔한 실수

### 9.1 흔한 보안 실수 TOP 10

| #   | 실수                                           | 위험도  | 올바른 방법                                   |
| :-- | :--------------------------------------------- | :------ | :-------------------------------------------- |
| 1   | `localStorage`에 JWT 저장                      | 🔴 높음 | HttpOnly 쿠키 또는 인메모리 토큰 사용         |
| 2   | `dangerouslySetInnerHTML`에 미검증 데이터 주입 | 🔴 높음 | DOMPurify로 반드시 새니타이징                 |
| 3   | API 키를 `NEXT_PUBLIC_`으로 노출               | 🔴 높음 | 서버 사이드에서만 사용, 프록시 구성           |
| 4   | CORS에서 `origin: "*"` 사용                    | 🟡 중간 | 허용 출처를 명시적으로 지정                   |
| 5   | CSP 미설정                                     | 🟡 중간 | 프로덕션에 반드시 CSP 적용                    |
| 6   | 의존성 버전 범위 지정자(`^`, `~`)              | 🟡 중간 | 정확한 버전 고정 (`save-exact`)               |
| 7   | `.env` 파일 Git 커밋                           | 🔴 높음 | `.gitignore`에 추가, `.env.example`만 커밋    |
| 8   | 에러 메시지에 스택 트레이스 노출               | 🟡 중간 | 프로덕션에서는 사용자 친화적 메시지만 표시    |
| 9   | 보안 헤더 미설정                               | 🟡 중간 | 최소 HSTS, X-Frame-Options, nosniff 설정      |
| 10  | Refresh Token 재사용 허용                      | 🔴 높음 | Token Rotation 적용, 재사용 시 모든 토큰 폐기 |

### 9.2 프로덕션 전 점검 사항

```typescript
// ❌ 개발 환경에서만 허용되는 코드가 프로덕션에 남아있지 않은지 확인

// 1. console.log에 민감한 정보 출력
console.log('user token:', token) // 🚨 절대 금지
console.log('request body:', requestBody) // 🚨 개인정보 포함 가능

// 2. 디버그용 CORS 설정
app.use(cors({ origin: '*' })) // 🚨 프로덕션에서 사용 금지

// 3. 에러 상세 정보 노출
app.use((err, req, res, next) => {
  res.status(500).json({
    error: err.message,
    stack: err.stack, // 🚨 프로덕션에서 스택 트레이스 노출 금지
  })
})

// ✅ 프로덕션 에러 핸들러
app.use((err, req, res, next) => {
  // 로그에는 상세 정보 기록
  logger.error('Internal error', { error: err, requestId: req.id })

  // 클라이언트에는 최소한의 정보만 전달
  res.status(500).json({
    error: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
    requestId: req.id, // 디버깅을 위한 추적 ID만 제공
  })
})
```

### 9.3 서드파티 스크립트 관리

```tsx
// ❌ 위험 — 외부 스크립트 무분별 삽입
<script src="https://unknown-analytics.com/tracker.js" />

// ✅ 안전 — Subresource Integrity (SRI)로 무결성 검증
<script
  src="https://cdn.example.com/lib.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"
  crossOrigin="anonymous"
/>

// Next.js에서의 서드파티 스크립트 관리
import Script from "next/script";

<Script
  src="https://analytics.example.com/tracker.js"
  strategy="afterInteractive" // 페이지 로드 후 로딩
/>
```

---

## AI 보조 보안 설계 검증

AI 사용 정책과 검증 책임은 [18. AI 개발 워크플로우](./18_AI_개발_워크플로우_종합.md)를 따릅니다. AI가 만든 보안 제안은 threat model, 재현 테스트, 보안 owner 승인 없이 적용하지 않습니다.

| 시나리오       | 입력                                   | AI 산출물                    | 필수 검증                            |
| :------------- | :------------------------------------- | :--------------------------- | :----------------------------------- |
| 인증 로직 리뷰 | auth flow, token storage, refresh path | race condition/XSS/CSRF 후보 | auth E2E, threat model 업데이트      |
| CSP 정책 초안  | 외부 script/style/img 출처 목록        | report-only CSP 후보         | violation report, staged enforcement |
| 의존성 점검    | lockfile, SBOM, package diff           | 취약/미관리 dependency 후보  | audit/provenance 확인, owner 승인    |
| 에러 노출 점검 | error handler, API response fixture    | 정보 노출 위험 후보          | production error snapshot test       |

---

## 체크리스트

### XSS 방어

- [ ] `dangerouslySetInnerHTML`을 사용 중인 곳에 `DOMPurify`가 적용되었나요?
- [ ] `href`, `src` 등에 사용자 입력이 들어가는 곳에서 `javascript:` 프로토콜을 필터링하나요?
- [ ] 웹 사이트 전체에 적절한 **CSP** 정책이 수립되었나요?
- [ ] CSP에서 `'unsafe-inline'`, `'unsafe-eval'`을 최소화했나요?
- [ ] `require-trusted-types-for 'script'`로 DOM XSS 싱크를 차단했나요? (Chrome/Edge 안정 지원)
- [ ] 서버 사이드에서도 HTML 새니타이징을 적용하고 있나요?

### 토큰 관리 / 인증

- [ ] 민감한 토큰이 `localStorage`가 아닌 `HttpOnly` 쿠키에 저장되어 있나요?
- [ ] Access Token은 인메모리에만 보관하고 있나요?
- [ ] Silent Refresh 로직에서 경쟁 상태(Race Condition)를 방지하고 있나요?
- [ ] Refresh Token Rotation이 적용되어 있나요?
- [ ] OAuth/OIDC는 RFC 9700 기준 Authorization Code + PKCE, exact redirect URI, `state`/`nonce`, issuer 검증을 따르나요?
- [ ] JWT는 `alg: none` 금지, 알고리즘 화이트리스트, `iss`/`aud`/`kid` 검증 (RFC 8725)이 적용되어 있나요?
- [ ] 신규 인증 플로우에 **Passkeys(WebAuthn)** 도입을 검토했고, 백업 인증 수단을 제공하나요?
- [ ] 토큰 만료 시 적절한 재인증 UX가 제공되나요?

### CSRF 방어

- [ ] 쿠키에 `SameSite=Lax` 이상이 설정되어 있나요?
- [ ] 상태 변경 API에 CSRF 토큰 또는 커스텀 헤더 검증이 적용되어 있나요?

### CORS

- [ ] `Access-Control-Allow-Origin`에 `*`가 아닌 구체적인 출처가 설정되어 있나요?
- [ ] `withCredentials` 사용 시 출처가 명시되어 있나요?

### 의존성 보안

- [ ] CI에서 `npm ci`를 사용하고 있나요?
- [ ] `npm audit`이 CI 파이프라인에 통합되어 있나요?
- [ ] 의존성 버전이 정확히 고정(`save-exact`)되어 있나요?
- [ ] 새로운 의존성 추가 시 보안 리뷰를 거치나요?

### 환경 변수 및 시크릿

- [ ] 서버 전용 시크릿에 `NEXT_PUBLIC_` 접두사가 없나요?
- [ ] `.env` 파일이 `.gitignore`에 포함되어 있나요?
- [ ] 환경 변수가 Zod 등으로 타입 검증되고 있나요?
- [ ] 빌드 결과물에 시크릿이 포함되지 않았는지 검사하나요?

### 보안 헤더

- [ ] API 응답 헤더에 `X-Content-Type-Options: nosniff`가 포함되어 있나요?
- [ ] `Strict-Transport-Security` (HSTS)가 설정되어 있나요?
- [ ] CSP의 `frame-ancestors` 또는 `X-Frame-Options`로 클릭재킹을 차단했나요?
- [ ] `Referrer-Policy`가 설정되어 있나요?
- [ ] `Permissions-Policy`로 불필요한 브라우저 기능을 차단했나요?
- [ ] `Cross-Origin-Opener-Policy`, `Cross-Origin-Resource-Policy`로 cross-origin 격리를 설정했나요?
- [ ] OAuth/결제 팝업이 있다면 COOP 값으로 `restrict-properties`를 고려했나요?
- [ ] `X-Powered-By` 헤더가 제거되었나요?

### 일반 보안

- [ ] 입력 폼에 `maxLength` 제한 및 서버 사이드 검증이 중복으로 되어 있나요?
- [ ] 프로덕션 빌드에서 `console.log`로 민감한 정보가 출력되지 않나요?
- [ ] 에러 응답에 스택 트레이스가 노출되지 않나요?
- [ ] 서드파티 스크립트에 SRI(Subresource Integrity)가 적용되어 있나요?
- [ ] SBOM, provenance/attestation, checksum이 release artifact와 함께 보관되나요?

---

## 15. secretlint 운영 기준

시크릿은 코드 리뷰에서 사람이 찾기 어렵기 때문에 로컬 hook과 CI에서 모두 빠르게 실패해야 합니다.

### 15.1 기본 구성

```json
{
  "rules": [
    {
      "id": "@secretlint/secretlint-rule-preset-recommend"
    }
  ]
}
```

- `.secretlintrc.json` 또는 `.secretlintrc.yml`을 저장소 root에 둡니다.
- `.secretlintignore`에는 generated artifact, public fixture처럼 의도적으로 검사하지 않을 경로만 넣습니다.
- secretlint는 기본적으로 secret 값을 mask하므로 CI 로그에 원문 secret이 노출되지 않게 합니다.
- `.env.example`에는 실제 값 대신 형식만 남기고, `.env.local`은 `.gitignore`와 secretlint 검사 범위에서 모두 관리합니다.

### 15.2 허용 목록 운영

- false positive는 `allowMessageIds` 또는 rule option으로 좁게 허용합니다.
- 허용 목록에는 owner, 이유, 만료일을 주석이나 ADR에 남깁니다.
- credential처럼 실제 위험이 있는 값은 allow하지 않고 즉시 rotate한 뒤 git history 노출 여부를 확인합니다.

### 15.3 gate 배치

| 위치            | 명령                                | 실패 시 처리                         |
| :-------------- | :---------------------------------- | :----------------------------------- |
| 로컬 pre-commit | `secretlint "**/*"`                 | commit 차단, staged file에서 값 제거 |
| PR CI           | `secretlint --format=github "**/*"` | merge 차단, annotation으로 위치 확인 |
| release         | secret scan + provenance 확인       | artifact 승격 중단                   |

---

## 실무 적용 가이드

### 언제 이 문서를 펼칠까

- 인증, 결제, 개인정보, 외부 스크립트, HTML 삽입을 다룰 때
- dependency audit 또는 secret scan 경고가 반복될 때
- CSP, cookie, token 저장 위치를 결정해야 할 때

### 적용 순서

1. 데이터 흐름에서 민감정보가 브라우저, 로그, URL, storage, third-party로 이동하는 지점을 표시한다.
2. 인증 토큰 저장 위치와 만료/갱신/폐기 전략을 정한다.
3. HTML 삽입, 외부 스크립트, iframe은 CSP와 Trusted Types 관점에서 검토한다.
4. dependency, secret, license, provenance 검사를 CI에 둔다.
5. 보안 예외는 owner와 만료일이 있는 RFC로만 허용한다.

### 함께 두는 파일

- feature별 보안 adapter, validation schema, 보안 테스트를 기능 폴더에 둔다.
- 공통 security helper는 낮은 수준의 encoder, sanitizer, header builder처럼 도메인 무관한 것만 둔다.
- 환경 변수 schema와 사용처를 가까이 둬 노출 범위를 찾기 쉽게 한다.

### 흔한 실수

- access token을 localStorage에 저장한다.
- HTML sanitizer 없이 CMS/사용자 입력을 렌더링한다.
- 보안 헤더를 인프라 문서에만 두고 실제 응답 검사를 하지 않는다.
- AI나 로그에 원본 개인정보를 전달한다.

### PR 완료 기준

- [ ] secret scan과 dependency audit 결과가 있다.
- [ ] 민감정보가 URL/log/client bundle에 남지 않는다.
- [ ] CSP/header 변경 영향이 검증되었다.
- [ ] 보안 예외에 owner와 만료일이 있다.

## 추천 항목 실행 우선순위 매핑

- `P1(7일 내)` — CSP, 인증, 시크릿, 공급망 통제 중 하나를 작은 변경 1건에 적용하고 증거(security scan)를 남긴다.
- `P2(30일 내)` — 웹 보안 기준을 팀 템플릿, 체크리스트, CI 중 한 곳에 고정한다.
- `P3(90일 내)` — 차단된 secret 수, CSP report, 취약 dependency 수 추이를 보고 기준을 유지할지 조정할지 결정한다.
- `완료 기준` — 보안 오너가 증거와 철회 조건을 확인했다는 기록을 남긴다.

## 추천 항목 실행 체크리스트

- [ ] `1단계(7일)` : CSP, 인증, 시크릿, 공급망 통제 적용 대상을 1개로 좁힌다.
- [ ] `2단계(30일)` : 증거(security scan, header capture, secretlint 결과, SBOM/attestation)를 PR, ADR, 회고 중 한 곳에 연결한다.
- [ ] `3단계(60일)` : 차단된 secret 수, CSP report, 취약 dependency 수가 기준 안에 들어왔는지 확인한다.
- [ ] `문제 대응` : 미달성 사유와 다음 조치, 중단 여부를 같은 기록에 남긴다.

## 추천 항목 실행 운영 규칙

- `실행 게이트` : 브라우저에 노출되는 값과 서버 전용 값을 먼저 분리한다.
- `승인 체계` : 보안 오너가 영향 범위와 rollback 담당자를 적용 전에 확인한다.
- `재개 조건` : 보안 헤더와 secret scan이 통과하면 배포 경로로 올린다.
- `정지 조건` : credential 노출, unsigned artifact, high 취약점이 있으면 즉시 중단한다.
- `리스크 점수` : 노출 표면, 권한 범위, 취약점 심각도로 산정한다.
- `리더 승인자` : 보안 리드가 최종 승인 책임을 맡는다.
- `승인 역할` : 웹 보안 작성자, 검토자, 운영 확인자를 분리해 기록한다.
- `재평가 주기` : 릴리스 전과 dependency bump 후에 다시 검토한다.
