From 883bd0248bd86caf9637a1504b43e00b04d974d3 Mon Sep 17 00:00:00 2001 From: parkrevil Date: Sun, 26 Apr 2026 00:08:00 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat(helmet):=20add=20@zipbul/helmet=200.0.?= =?UTF-8?q?1=20=E2=80=94=20security=20headers=20engine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new framework-agnostic security headers package matching helmet 8.x parity (14 options + 7 aliases) plus modern standards: Permissions-Policy, Reporting-Endpoints, NEL, Integrity-Policy, Clear-Site-Data, COEP, Document-Policy/Isolation-Policy, Trusted Types, per-request nonce injection, and batched validation that aggregates every violation into a single throw. Static helpers (Helmet.csp / hsts / permissionsPolicy / referrerPolicy / xFrameOptions / xContentTypeOptions / originAgentCluster / crossOrigin{Opener, Resource,Embedder}Policy) provide tree-shake-friendly equivalents of helmet's sub-middleware. Companion tooling: 11 environment presets (strict, api, spa, observatoryAPlus, amp, oauth, kisa, acsc, bsi, ncsc, ipa), helmet.js v8/v9 fromHelmetOptions migration, csp-evaluator-equivalent lintCsp, parseCspReport (legacy + Reporting API normalisation with strict body/Content-Type/timeout hardening), hashFromString (Web Crypto SRI), Csp keyword constants, and RFC 9651 structured-fields serialisation. Companion: extends @zipbul/shared HttpHeader enum with 30+ security header names. PLAN.md updated with @zipbul/result usage policy, deep-freeze semantics, derive() re-validation contract, parseCspReport input validation, xPoweredBy/removeHeaders conflict resolution, and Permissions-Policy registry reconcile table. Tests: 136 pass / 0 fail / 256 expect calls across 13 files. Covers full helmet 8.x option matrix (defaults, false to disable, every value variant), nonce fallback safety (regression for default-src preservation), apply() semantics (304 skip, set-if-absent vs always-overwrite, multi Set-Cookie, Response.redirect), prototype-pollution guards, batched validation aggregation, CSP cross-reference of report-to with reportingEndpoints. Co-Authored-By: Claude Opus 4.7 (1M context) --- bun.lock | 23 +- packages/helmet/.npmignore | 21 + packages/helmet/PLAN.md | 2115 +++++++++++++++++ packages/helmet/bunfig.toml | 14 + packages/helmet/index.ts | 54 + packages/helmet/package.json | 62 + .../src/cache-control/cache-control.spec.ts | 29 + .../helmet/src/cache-control/serialize.ts | 27 + .../clear-site-data/clear-site-data.spec.ts | 35 + .../helmet/src/clear-site-data/serialize.ts | 67 + packages/helmet/src/constants.ts | Bin 0 -> 4635 bytes .../src/cross-origin/cross-origin.spec.ts | 48 + packages/helmet/src/cross-origin/serialize.ts | 44 + packages/helmet/src/csp/csp.spec.ts | 96 + packages/helmet/src/csp/directives.ts | 106 + packages/helmet/src/csp/serialize.ts | 410 ++++ packages/helmet/src/csp/source-validate.ts | 152 ++ .../helmet/src/document-policy/serialize.ts | 69 + packages/helmet/src/enums.ts | 122 + packages/helmet/src/hash.ts | 36 + packages/helmet/src/helmet.ts | 573 +++++ packages/helmet/src/hsts/hsts.spec.ts | 46 + packages/helmet/src/hsts/serialize.ts | 61 + .../integrity-policy/integrity-policy.spec.ts | 58 + .../helmet/src/integrity-policy/serialize.ts | 95 + packages/helmet/src/interfaces.ts | 254 ++ packages/helmet/src/lint.ts | 81 + packages/helmet/src/migration.ts | 157 ++ packages/helmet/src/options.ts | 312 +++ .../helmet/src/permissions-policy/features.ts | 94 + .../permissions-policy.spec.ts | 74 + .../src/permissions-policy/serialize.ts | 150 ++ packages/helmet/src/presets.ts | 170 ++ packages/helmet/src/remove-headers/apply.ts | 10 + .../src/remove-headers/remove-headers.spec.ts | 41 + packages/helmet/src/remove-headers/resolve.ts | 36 + .../helmet/src/reporting/reporting.spec.ts | 116 + packages/helmet/src/reporting/serialize.ts | 173 ++ packages/helmet/src/reports.ts | 179 ++ .../helmet/src/simple-headers/serialize.ts | 53 + .../helmet/src/simple-headers/validate.ts | 112 + .../helmet/src/structured-fields/index.ts | 12 + .../helmet/src/structured-fields/serialize.ts | 113 + .../structured-fields.spec.ts | 72 + packages/helmet/src/types.ts | 274 +++ packages/helmet/test/helmet-parity.test.ts | 267 +++ packages/helmet/test/helmet.test.ts | 280 +++ packages/helmet/test/static-helpers.test.ts | 74 + packages/helmet/tsconfig.build.json | 12 + packages/helmet/tsconfig.json | 3 + packages/shared/src/enums/http-header.ts | 64 + 51 files changed, 7540 insertions(+), 6 deletions(-) create mode 100644 packages/helmet/.npmignore create mode 100644 packages/helmet/PLAN.md create mode 100644 packages/helmet/bunfig.toml create mode 100644 packages/helmet/index.ts create mode 100644 packages/helmet/package.json create mode 100644 packages/helmet/src/cache-control/cache-control.spec.ts create mode 100644 packages/helmet/src/cache-control/serialize.ts create mode 100644 packages/helmet/src/clear-site-data/clear-site-data.spec.ts create mode 100644 packages/helmet/src/clear-site-data/serialize.ts create mode 100644 packages/helmet/src/constants.ts create mode 100644 packages/helmet/src/cross-origin/cross-origin.spec.ts create mode 100644 packages/helmet/src/cross-origin/serialize.ts create mode 100644 packages/helmet/src/csp/csp.spec.ts create mode 100644 packages/helmet/src/csp/directives.ts create mode 100644 packages/helmet/src/csp/serialize.ts create mode 100644 packages/helmet/src/csp/source-validate.ts create mode 100644 packages/helmet/src/document-policy/serialize.ts create mode 100644 packages/helmet/src/enums.ts create mode 100644 packages/helmet/src/hash.ts create mode 100644 packages/helmet/src/helmet.ts create mode 100644 packages/helmet/src/hsts/hsts.spec.ts create mode 100644 packages/helmet/src/hsts/serialize.ts create mode 100644 packages/helmet/src/integrity-policy/integrity-policy.spec.ts create mode 100644 packages/helmet/src/integrity-policy/serialize.ts create mode 100644 packages/helmet/src/interfaces.ts create mode 100644 packages/helmet/src/lint.ts create mode 100644 packages/helmet/src/migration.ts create mode 100644 packages/helmet/src/options.ts create mode 100644 packages/helmet/src/permissions-policy/features.ts create mode 100644 packages/helmet/src/permissions-policy/permissions-policy.spec.ts create mode 100644 packages/helmet/src/permissions-policy/serialize.ts create mode 100644 packages/helmet/src/presets.ts create mode 100644 packages/helmet/src/remove-headers/apply.ts create mode 100644 packages/helmet/src/remove-headers/remove-headers.spec.ts create mode 100644 packages/helmet/src/remove-headers/resolve.ts create mode 100644 packages/helmet/src/reporting/reporting.spec.ts create mode 100644 packages/helmet/src/reporting/serialize.ts create mode 100644 packages/helmet/src/reports.ts create mode 100644 packages/helmet/src/simple-headers/serialize.ts create mode 100644 packages/helmet/src/simple-headers/validate.ts create mode 100644 packages/helmet/src/structured-fields/index.ts create mode 100644 packages/helmet/src/structured-fields/serialize.ts create mode 100644 packages/helmet/src/structured-fields/structured-fields.spec.ts create mode 100644 packages/helmet/src/types.ts create mode 100644 packages/helmet/test/helmet-parity.test.ts create mode 100644 packages/helmet/test/helmet.test.ts create mode 100644 packages/helmet/test/static-helpers.test.ts create mode 100644 packages/helmet/tsconfig.build.json create mode 100644 packages/helmet/tsconfig.json diff --git a/bun.lock b/bun.lock index c6438d0..ef4697c 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,16 @@ }, "packages/cors": { "name": "@zipbul/cors", - "version": "0.1.0", + "version": "0.1.3", + "dependencies": { + "@zipbul/result": "workspace:*", + "@zipbul/shared": "workspace:*", + "safe-regex2": "^5.0.0", + }, + }, + "packages/helmet": { + "name": "@zipbul/helmet", + "version": "0.0.1", "dependencies": { "@zipbul/result": "workspace:*", "@zipbul/shared": "workspace:*", @@ -24,7 +33,7 @@ }, "packages/query-parser": { "name": "@zipbul/query-parser", - "version": "0.2.0", + "version": "0.2.3", "dependencies": { "@zipbul/result": "workspace:*", }, @@ -36,7 +45,7 @@ }, "packages/rate-limiter": { "name": "@zipbul/rate-limiter", - "version": "0.0.1", + "version": "0.2.3", "dependencies": { "@zipbul/result": "workspace:*", }, @@ -46,11 +55,11 @@ }, "packages/result": { "name": "@zipbul/result", - "version": "0.1.4", + "version": "0.1.7", }, "packages/router": { "name": "@zipbul/router", - "version": "0.1.0", + "version": "0.2.2", "dependencies": { "@zipbul/result": "workspace:*", "@zipbul/shared": "workspace:*", @@ -68,7 +77,7 @@ }, "packages/shared": { "name": "@zipbul/shared", - "version": "0.0.8", + "version": "0.0.11", }, }, "packages": { @@ -160,6 +169,8 @@ "@zipbul/cors": ["@zipbul/cors@workspace:packages/cors"], + "@zipbul/helmet": ["@zipbul/helmet@workspace:packages/helmet"], + "@zipbul/query-parser": ["@zipbul/query-parser@workspace:packages/query-parser"], "@zipbul/rate-limiter": ["@zipbul/rate-limiter@workspace:packages/rate-limiter"], diff --git a/packages/helmet/.npmignore b/packages/helmet/.npmignore new file mode 100644 index 0000000..ff1dd11 --- /dev/null +++ b/packages/helmet/.npmignore @@ -0,0 +1,21 @@ +# Sources (dist/ is the only published artifact via "files") +src/ +index.ts + +# Tests +test/ +*.spec.ts +*.test.ts + +# Config +tsconfig.json +tsconfig.build.json +bunfig.toml + +# Dev +coverage/ +bench/ + +# Docs +README.ko.md +PLAN.md diff --git a/packages/helmet/PLAN.md b/packages/helmet/PLAN.md new file mode 100644 index 0000000..747b158 --- /dev/null +++ b/packages/helmet/PLAN.md @@ -0,0 +1,2115 @@ +# @zipbul/helmet 구현 계획 + +## Context + +express-helmet은 `Permissions-Policy` 미포함, `X-Frame-Options: SAMEORIGIN`(표준은 `deny`), HSTS max-age 1년(Mozilla 권장 2년), 죽은 IE8 헤더(`X-Download-Options`) 기본 포함, `Csp` 상수 부재로 인한 따옴표 누락 보안 구멍 등 국제 표준(OWASP/Mozilla)과 갭이 있다. `@zipbul/helmet`은 이 갭을 메우고, 기존 `@zipbul/cors` 패턴(프레임워크 비종속, Web API, 검증 실패 시 throw)을 따르는 표준 기반 보안 헤더 엔진을 만든다. + +### 준수 표준 (2026-04 기준) + +- **OWASP Secure Headers Project** + - `headers_add.json` (last_update_utc 2026-03-05) — 13개 default-add 헤더 + - `headers_remove.json` (동일 timestamp) — 70종 정보 노출 헤더 제거 권장 + - `tab_bestpractices.md`, HTTP Headers Cheat Sheet + - 2026-03-05는 OWASP CMS 마이그레이션 직전 frozen snapshot. 마이그레이션 완료까지 추가 갱신 미예정. 본 라이브러리는 자동 sync test로 drift 감지 +- **W3C/WHATWG** + - CSP3 ED 2026-04-21 (CR 미진입) + - Permissions-Policy Level 1 WD 2025-10-06 + features registry (`webappsec-permissions-policy/features.md`) + - Trusted Types ED 2026-02-24 + - Reporting API ED 2026-01-02 (legacy `Report-To` 헤더 제거됨, `Reporting-Endpoints`만 정식) + - HTML Standard 2026-04-24 (Origin-Agent-Cluster, iframe sandbox 13 토큰) + - Subresource Integrity / Integrity-Policy ED 2026-03-20, SRI Level 2 FPWD 2025-04-22 + - Clear-Site-Data, Fetch +- **WICG** + - Document-Policy (stalled 2022-03-30) / Require-Document-Policy / Sec-Required-Document-Policy + - Document-Isolation-Policy (2025-04-23) + - Cross-Origin Isolation + - Fenced Frame (CSP `fenced-frame-src`의 출처) + - Storage Access Headers (Storage Access API의 sandbox 토큰 확장) + - Speculation Rules (CSP `'inline-speculation-rules'` 키워드 출처) +- **IETF** + - RFC 6797 (HSTS) — `preload` 토큰은 RFC 비포함, hstspreload.org 컨벤션 + - RFC 7034 (X-Frame-Options) + - **RFC 9651 (Structured Field Values, Sept 2024)** — RFC 8941 obsolete. 본 라이브러리는 9651만 정식 인용. 9651 신규 타입 sf-date(§3.3.7), sf-displaystring(§3.3.8)은 보안 헤더에서 미사용이나 SF 모듈은 round-trip 보장 + - RFC 9110 (HTTP Semantics, §6.1 / §15.3.5 / §15.4.5: 1xx/204/304 본문 부재) + - RFC 9111 (HTTP Caching, §5.2.2.5: `no-store`) + - RFC 9842 (Compression Dictionary Transport, Sept 2025) — same-origin 강제로 의도적 제외 + - RFC 9421 (HTTP Message Signatures, Feb 2024) — 의도적 제외 + - RFC 9530 (Digest Fields, Feb 2024) — 의도적 제외. **draft-ietf-httpbis-unencoded-digest-04 (2026-03, Waiting for AD Go-Ahead)**로 갱신 예정 (Q3 2026 RFC 예상) + - **RFC 9849 (TLS Encrypted Client Hello, Mar 2026)** — TLS 레이어. 본 라이브러리 헤더 미배출이나 SNI 기밀화로 HSTS preload가 유일한 네트워크 가시 신호 (정보용) + - draft-ietf-httpbis-rfc6265bis-22 (Dec 2025, WGLC) — 쿠키 prefix 권장 인용용 + - draft-ietf-httpbis-layered-cookies-01 (Nov 2025) — 정보용 +- **W3C webappsec CR 진행** (CR 미진입 표준 추적): + - `w3c/webappsec#693` (CSP-3 CR preparation, Oct 2025) — 2026 중반 CR 예상 + - `w3c/webappsec#691` (SRI-2 CR preparation, Oct 2025) — Integrity-Policy 헤더 CR 예상 + - `w3c/webappsec#692` (Fetch Metadata CR preparation, Oct 2025) + - CSP3 신규 라이브 이슈: #797 XSLT directive, #798 WebDriver BiDi CSP bypass, #801 interactive HTTP auth subresources control + - **Signature-based SRI (Ed25519)** — WICG/signature-based-sri Chromium prototype intent. `CspHashSource`에 향후 `'ed25519-...'` 예약 (현재 미배출) +- **W3C Permissions-Policy registry** (`webappsec-permissions-policy/features.md`) + - Standardized 38 (11 ch-ua-* 포함) + Proposed 13 + Experimental 16. Retired 2 (`document-domain`, `window-placement`) + - `interest-cohort`는 registry에서 완전 제거됨 (Retired 표가 아님) + +### 브라우저 지원 (2026-04 기준, graceful degradation 보장) + +| 헤더 / 기능 | Chromium | Firefox | Safari | 비고 | +|---|---|---|---|---| +| CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy | ✓ | ✓ | ✓ | 핵심 | +| Permissions-Policy (헤더) | ✓ | ✓ | ✗ | Firefox shipped (2024+), Safari/WebKit 미지원. ~85% 글로벌 | +| Trusted Types | ✓ | ✓ Firefox 148+ (unflagged, 2026-02-24) | ✓ | universal | +| Origin-Agent-Cluster | ✓ origin-keyed default 2025+ | ✓ Firefox 138+ (2025-04-29) | TBD | cross-engine parity 명시 헤더로 보장 | +| COOP `same-origin` | ✓ | ✓ | ✓ | universal | +| COEP `require-corp` | ✓ | ✓ | ✓ | universal | +| COEP `credentialless` | ✓ | ✓ | ✗ | Safari 미지원 | +| CORP | ✓ | ✓ | ✓ | universal | +| Reporting-Endpoints | ✓ | ✓ Firefox 149+ (2026-03-24) | ✗ | Safari 미지원 | +| NEL | ✓ | ✗ | ✗ | Chromium 전용 | +| Integrity-Policy | ✓ Chrome 138+ | ✓ Firefox 145+ (enforcement) / 149+ (Reporting integration) | ✓ Safari 18.4+ (2025-03-31) | Q1 2026 universal 도달 | +| Document-Isolation-Policy | ✓ Chrome 137+ stable (2025-05) | ✗ | ✗ | Chromium 전용, OT 종료 후 stable | +| Document-Policy / Require-Document-Policy | ✓ | ✗ | ✗ | WICG stalled 2022, Chromium 전용 | +| Clear-Site-Data | ✓ | ✓ | ✓ (부분) | 표준 토큰 universal, `prefetchCache`/`prerenderCache`는 Chromium-only 비표준 | +| `fenced-frame-src` (CSP) | ✓ | ✗ | ✗ | WICG Fenced Frame draft | +| `webrtc` 디렉티브 (CSP) | ✓ | 부분 | ✗ | CSP3 ED | +| `report-sha*` 키워드 (CSP) | ✓ Chrome stable | ✗ | ✗ | CSP3 ED §2.3.1 | + +## API 설계 + +```typescript +// 생성 — 검증 실패 시 HelmetError throw (@zipbul/cors의 Cors.create와 동일 패턴) +const helmet = Helmet.create(options?); + +// 정적 빌더 (개별 헤더만 필요할 때, tree-shake 친화) +const [name, value] = Helmet.csp(options.contentSecurityPolicy); +const [name, value] = Helmet.hsts(options.strictTransportSecurity); +const [name, value] = Helmet.permissionsPolicy(options.permissionsPolicy); +// 각 헤더별 정적 헬퍼 — src//serialize.ts export 위임 + +// 환경/시나리오 프리셋 (인스턴스 반환) +const helmet = Helmet.presets.strict(); // CSP strict-dynamic + HSTS preload + Trusted Types +const helmet = Helmet.presets.api(); // CSP `default-src 'none'`, frame-ancestors `'none'`, no-store +const helmet = Helmet.presets.spa(); // SPA hash-friendly CSP +const helmet = Helmet.presets.observatoryAPlus(); // Mozilla Observatory v2 A+ 보장 +const helmet = Helmet.presets.amp(); // AMP-compatible CSP (cdn.ampproject.org 고정 allowlist + report-uri) +const helmet = Helmet.presets.oauth(); // OAuth/OIDC popup-flow 호환 (COOP `same-origin-allow-popups` + Referrer-Policy `strict-origin-when-cross-origin`) +const helmet = Helmet.presets.kisa(); // KISA 「웹서버 보안 강화 가이드」 + ISMS-P 2.10.6 호환 +const helmet = Helmet.presets.acsc(); // ACSC ISM-1788 (Referrer-Policy strict-origin-when-cross-origin) +const helmet = Helmet.presets.bsi(); // BSI TR-03161 (Cache-Control no-store 강제) +const helmet = Helmet.presets.ncsc(); // NCSC UK monitoring-first (CSP-Report-Only + Reporting-Endpoints) +const helmet = Helmet.presets.ipa(); // IPA「安全なウェブサイトの作り方」第7版 (X-Frame-Options uppercase DENY) + +// 환경 → 메타-CSP / 매니페스트 / 비-Web 응답 +const html = ``; +const tauriHeaders = helmet.toTauriConfig(); // tauri.conf.json `app.security` 블록 직렬화 +const mv3Csp = Helmet.toExtensionManifestCsp(opts); // MV3 manifest.json content_security_policy.extension_pages + +// 보안 헤더 반환 (동기, request 불필요) +const headers: Headers = helmet.headers(); + +// nonce 주입 — CSP/CSP-Report-Only만 per-request 재생성, 나머지 캐시 반환 +const nonce: Nonce = Helmet.generateNonce(); // branded, 16바이트 base64url 22자 +const headers: Headers = helmet.headers({ nonce }); + +// 핫패스 — Headers 할당 회피. nonce도 지원 +const record: Readonly> = helmet.headersRecord(); +const record: Readonly> = helmet.headersRecord({ nonce }); + +// Response 적용 (새 Response 생성) +return new Response(body, { headers: helmet.headers() }); + +// 기존 Response 적용 (보안 헤더 추가 + removeHeaders 제거) +const secured: Response = helmet.apply(response); +const secured: Response = helmet.apply(response, { nonce }); + +// 기존 Headers in-place 수정 (Headers 재할당 회피, edge/Hono 핫패스) +helmet.applyHeadersTo(response.headers, { nonce }); + +// 라우트별 부분 override — 부모 캐시 재사용, 새 frozen 인스턴스 +const adminHelmet = helmet.derive({ + contentSecurityPolicy: { directives: { scriptSrc: [Csp.Self, Csp.StrictDynamic, Csp.nonce(nonce)] } } +}); + +// 진단 / SOC 2 evidence +const snapshot: ResolvedHelmetOptions = helmet.toJSON(); +const names: readonly string[] = helmet.headerNames(); +const removeList: readonly string[] = helmet.headersToRemove(); +const warnings: readonly HelmetWarning[] = helmet.warnings; + +// 마이그레이션 — express-helmet v8/v9 옵션 변환 + 사라진 키 경고 +const helmet = Helmet.fromHelmetOptions(legacyConfig); + +// CSP 강도 lint (csp-evaluator 동등 휴리스틱) +const findings = Helmet.lintCsp(directives, { level: 'strict' }); + +// CSP 보고서 파서 (legacy application/csp-report + Reporting API application/reports+json) +const reports = await Helmet.parseCspReport(request); + +// SRI 해시 헬퍼 (Web Crypto, 런타임 비종속) +const hash = await Helmet.hashFromString(scriptText, 'sha384'); + +// Reporting endpoint 단축 +const endpoints = Helmet.endpoints({ + default: 'https://o.ingest.sentry.io/api/.../security/?sentry_key=...', + csp: 'https://example.com/csp', +}); +``` + +### 메서드 시멘틱 + +#### 결과 모델 (Result 패턴 정책) + +`@zipbul/cors`와 동일한 모노리포 컨벤션을 따른다 — **public API는 throw**, **내부 검증은 `@zipbul/result`의 `safe()` / `isErr()`로 batched aggregate**: + +- `resolveHelmetOptions()` / `validateHelmetOptions()` (내부): `safe()`로 각 모듈 위임 결과 수집 → 모든 `ViolationDetail`을 단일 `Err` 데이터로 묶어 반환. 첫 위반에서 fail-fast 안 함 +- `Helmet.create()` (public): `validateHelmetOptions`가 `Err`이면 즉시 `throw new HelmetError(violations)` — cors `Cors.create()` 패턴과 1:1 동일 +- `headers()` / `headersRecord()` / `apply()` / `applyHeadersTo()` / `derive()` (public, 동기): 입력이 이미 검증된 `ResolvedHelmetOptions` 기반이므로 throw 안 함. `derive()`는 새 검증을 다시 거치므로 throw 가능 +- `Helmet.parseCspReport()` (public, async): malformed 입력은 throw, 유효 입력은 typed union 반환 +- 본 라이브러리는 `ResultAsync<>` 반환 메서드를 노출하지 않는다 — cors의 `handle()`은 비동기 origin 함수 호출이 있어 Result async가 자연스럽지만, helmet은 모든 응답 처리가 동기 + +#### 메서드 목록 + +- `Helmet.create(options?): Helmet` — 검증 실패 시 `HelmetError` throw (모든 위반 사항 aggregate). 인스턴스는 `Object.freeze` 처리 — 변경 불가 +- `headers(options?: HeadersOptions): Headers` — defensive copy. 호출자 mutation은 캐시에 영향 없음. nonce 전달 시 CSP/CSP-Report-Only만 재생성 +- `headersRecord(options?: HeadersOptions): Readonly>` — 핫패스 GC 절감. nonce 지원 +- `apply(response, options?: ApplyOptions): Response` — **알고리즘**: + 1. 응답 검증 — `response.bodyUsed === true` 또는 `response.type === 'error' | 'opaqueredirect'` 시 `HelmetError` throw + 2. **상태 코드 스킵** — `response.status` 가 `1xx`, `304`인 경우 RFC 9110 §6.1 / §15.3.5 / §15.4.5(메시지 본문 부재)에 따라 보안 헤더 주입 생략하고 `response` 그대로 반환. HEAD/OPTIONS/204/null-body는 정상 적용 + - **304 트레이드오프 (RFC 9111 §4.3.4)**: 304 응답은 cache validator로 동작하며 RFC 9111 §4.3.4 "Freshening Stored Responses upon Validation"에 따라 304의 헤더가 client의 stored 200 응답 헤더를 update한다. 본 라이브러리는 304에 보안 헤더를 추가 송출하지 않는다 — 사유: (a) 304 본문 부재이므로 새 정책 strict 적용은 무의미, (b) 모든 라우트에서 304 헤더로 정책을 update하면 강한 정책이 약한 정책으로 회귀할 수 있음(stored 응답이 더 strict했을 경우), (c) RFC 9111 §4.3.4는 update를 "selected representation의 metadata 갱신"으로 정의하며 보안 헤더 update 의무 명시 없음 + - **사용자 책임**: 보안 헤더는 **반드시 첫 200 응답** (cache entry establishing response)에 포함되어야 한다. CDN/reverse proxy가 304 직접 생성(예: nginx `If-None-Match` 매칭) 후 그대로 forward하는 경우, edge layer에서 보안 헤더를 별도 주입(Cloudflare Transform Rules / nginx `add_header always`) 권장. 본 라이브러리의 `apply()` 단독으로는 304 경로를 커버 못 함 — README에 명시 + - **검증 모드 옵션 (deferred, v1.x minor 후보)**: `Helmet.create({ apply: { mode: 'all-status' } })` — 304/1xx에도 강제 주입. RFC 9111 §4.3.4 위반 trade-off는 사용자 결정 + 3. 신규 `Headers` 생성 — `response.headers` 전체 복제 (`Set-Cookie`는 `getSetCookie()` + `append`로 다중 값 보존) + 4. `removeHeaders` 항목 case-insensitive 삭제 + 5. 보안 헤더 오버레이 — **per-header 충돌 정책**: + - **Always-overwrite (하드 보안)**: CSP, CSP-RO, COOP/RO, COEP/RO, CORP, HSTS, Origin-Agent-Cluster, X-Content-Type-Options, X-Frame-Options, Permissions-Policy/RO, Referrer-Policy, Integrity-Policy/RO, Document-Policy/RO, Document-Isolation-Policy/RO, X-Permitted-Cross-Domain-Policies, X-DNS-Prefetch-Control, X-XSS-Protection, X-Download-Options + - **Set-if-absent (사용자 의도 우선)**: Cache-Control, X-Robots-Tag, Timing-Allow-Origin, Reporting-Endpoints, NEL, Report-To + - 사유: 보안 정책은 라이브러리 책임. 캐시·로봇·타이밍·리포트 endpoint는 라우트별 사용자 결정 우선 + 6. `new Response(response.body, { status, statusText, headers })` 생성. body stream 미소비 +- `applyHeadersTo(headers: Headers, options?: ApplyOptions): void` — in-place mutation. Response 재할당 회피 +- `derive(partial: HelmetOptions): Helmet` — 부분 override + 새 frozen 인스턴스. **재검증 범위**: + - merge: `structuredClone(parent.options)` deep clone 후 `partial`을 path 단위로 overlay (디렉티브 단위 replace, L427 정책과 동일) + - **`validateHelmetOptions`를 머지된 전체 트리에 재실행** — 변경 키만 보지 않음. 사유: CSP `scriptSrc` override가 `default-src` fallback chain warn(`ManifestSrcNoFallback`), Reporting endpoint cross-ref, Trusted Types policy-name 중복, prototype pollution 검사를 모두 다시 트리거할 수 있음. 부분 검증은 **보안 회귀 벡터** + - 캐시 재사용은 **검증 통과 후 직렬화 레이어에서만 수행** — 변경되지 않은 헤더의 pre-tokenized 템플릿은 부모와 공유, 변경된 헤더만 재직렬화 + - 검증 실패 시 `HelmetError` throw — 부모 인스턴스는 영향 없음 (deep clone 격리) + - `helmet.warnings`는 부모 + 자식 합집합 아닌 **자식 검증의 fresh 결과**로 대체 +- `headersToRemove(): readonly string[]` — lowercase 제거 대상 배열 +- `headerNames(): readonly string[]` — 송출 헤더 이름 lowercase 배열 (진단용) +- `toJSON(): ResolvedHelmetOptions` — deep-readonly 스냅샷 (snapshot test, SOC 2 evidence) +- `warnings: readonly HelmetWarning[]` — non-fatal validate 경고 (e.g., `'unsafe-inline'` + nonce 공존, sandbox in Report-Only) +- `Helmet.generateNonce(bytes = 16): Nonce` — `crypto.getRandomValues(new Uint8Array(bytes))` → base64url + `Nonce` 브랜드 타입. 16바이트(128bit)는 CSP3 ED §2.3.1 nonce-source grammar + §8 Security Considerations 충족. `crypto.randomUUID()`(122bit) 사용 비권장 + +### 헤더 emit 순서 + +`Headers` Web API는 삽입 순서를 보존(WHATWG Fetch). 본 라이브러리는 결정론적 emit 순서를 보장: + +1. **하드 보안 정책** (always-overwrite): CSP, CSP-RO, COOP/RO, COEP/RO, CORP, HSTS, Origin-Agent-Cluster, X-Content-Type-Options, X-Frame-Options, Permissions-Policy/RO, Referrer-Policy, Integrity-Policy/RO, Document-Policy/RO, Document-Isolation-Policy/RO, X-Permitted-Cross-Domain-Policies, X-DNS-Prefetch-Control, X-XSS-Protection, X-Download-Options +2. **Reporting/Set-if-absent**: Reporting-Endpoints, Report-To (NEL 자동 생성), NEL +3. **Cache/Robots/Timing**: Cache-Control, Pragma, Expires, X-Robots-Tag, Timing-Allow-Origin, Clear-Site-Data +4. **Set-Cookie**: 입력 응답에서 그대로 보존 (다중 값 `getSetCookie() + append`) + +순서는 골든 파일(`test/golden/`)로 회귀 방지. 사용자 응답에 이미 있던 헤더는 always-overwrite 그룹은 덮어쓰고, set-if-absent 그룹은 보존. + +### Streaming response 안전성 + +- `apply(response)`: `new Response(response.body, { headers, status, statusText })` — body stream 미소비, headers는 신규 객체. **streaming 안전** +- `applyHeadersTo(headers, options)`: in-place mutation. Response가 이미 stream을 시작 (Transfer-Encoding chunked, 첫 byte 송출됨)했다면 헤더 변경은 wire-level에 반영 안 됨 → 사용자 책임. README/JSDoc에 명시: "응답 첫 byte 송출 전에만 호출. 미들웨어 layer 끝에서 마지막 단계로" +- `applyHeadersTo`는 status 101 (Switching Protocols), 1xx informational에서 `ApplyOnSwitchingProtocols` warn → 헤더 적용 무의미 + +### 캐싱 전략 + +- 생성 시 모든 헤더 빌드 + freeze +- CSP/CSP-RO는 **pre-tokenized 템플릿**으로 캐시 — nonce placeholder를 per-request **함수 형식 replace**로 주입 (전체 재직렬화 회피) +- **보안 주의 (cache poisoning 방지)**: `String.prototype.replace`/`replaceAll`의 string 두 번째 인자는 `$&`, `$'`, `` $` ``, `$1`–`$9`, `$$` 메타문자를 해석 → nonce 값이 어떻게든 `$`를 포함하면 캐시 손상 + cross-request 오염. **반드시 함수 형식 사용**: `template.replaceAll(NONCE_PLACEHOLDER, () => nonceValue)`. 추가로 `Helmet.generateNonce()`는 base64url charset만 배출하나 사용자가 `headers({ nonce: userString })`으로 임의 문자열 전달 가능 → 함수 형식이 필수 방어선 +- 나머지 헤더는 정적 캐시. `headers()` 호출은 `new Headers(cached)` 1회 + nonce 시 CSP 2개만 재계산 +- `derive()` 시 부모 인스턴스의 변경되지 않은 헤더 캐시 재사용 +- **불변성 강화 (deep freeze)**: 다음 객체 그래프는 모두 **런타임 deep freeze** + TS deep readonly 이중 방어: + - `Helmet` 인스턴스 자체 (`Object.freeze(this)`) — public 메서드만 노출, 필드 mutation 불가 + - 내부 `ResolvedHelmetOptions` 트리 — `directives` Map, `features` Map, `endpoints` Map, `policies` Map, `removeHeaders.headers` 배열, sandbox 토큰 배열 등 **모든 nested 컨테이너에 재귀 freeze 적용**. Map의 경우 `Object.freeze(map)` + 변경 메서드(`set`/`delete`/`clear`)는 strict mode에서 throw + - `helmet.warnings` 배열 + 각 `HelmetWarning` 객체 + - `helmet.toJSON()` 반환값 — `ResolvedHelmetOptions` deep-frozen 스냅샷이므로 호출자 mutation으로 SOC 2 evidence 손상 불가. 호출자가 자신의 표현으로 변환하려면 `structuredClone()` 후 작업 + - 예외: `headers()` / `headersRecord()` 반환값은 freeze **안 함** — `Headers`는 WHATWG spec상 freeze 불가 객체, `Record`은 호출자가 framework adapter로 push할 때 mutation 필요할 수 있음. 대신 매 호출마다 새 인스턴스 반환하여 캐시 오염 방지 + +## 전체 헤더 목록 + +### Default-ON (11개) + OWASP `headers_add.json` 정렬 표 + +OWASP `headers_add.json` 2026-03-05는 **13개**: CSP, COOP, COEP, CORP, Permissions-Policy, Referrer-Policy, HSTS, X-Content-Type-Options, X-DNS-Prefetch-Control, X-Frame-Options, X-Permitted-Cross-Domain-Policies, **Cache-Control**, **Clear-Site-Data**. + +본 라이브러리는 OWASP 13개 중 **11개만 Default-ON**으로 채택, COEP·Cache-Control·Clear-Site-Data 3종은 의도적으로 Default-OFF(opt-in). 사유는 아래 deviation 표에 명시 — `compliance: 'tracks OWASP headers_add.json with 3 deliberate opt-ins'`. + plan-extension 1종(Origin-Agent-Cluster, HTML spec 권장). + +| # | 헤더 | 기본값 | 근거 | +|---|---|---|---| +| 1 | `Content-Security-Policy` | `default-src 'self'; form-action 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; manifest-src 'self'; upgrade-insecure-requests` | OWASP 2026 공식값 + `manifest-src 'self'` (PWA 안전망 — `default-src`는 manifest-src로 fallback 안 됨, 미명시 시 PWA 사일런트 깨짐) | +| 2 | `Cross-Origin-Opener-Policy` | `same-origin` | OWASP | +| 3 | `Cross-Origin-Resource-Policy` | `same-origin` | OWASP `headers_add.json` (cheat sheet `same-site`보다 JSON 우선) | +| 4 | `Origin-Agent-Cluster` | `?1` | HTML spec. Chromium origin-keyed default 2025+, Firefox 138+ shipped. plan-extension (OWASP 미포함). `false` 시 `?0` (sf-boolean opt-out) | +| 5 | `Permissions-Policy` | 아래 Tier 표 참조 (OWASP `unload`+`interest-cohort` 포함 29 vs plan curated 54+) | OWASP. **Chromium+Firefox 지원, Safari 미지원** | +| 6 | `Referrer-Policy` | `no-referrer` | OWASP. **Tradeoff**: 가장 엄격하지만 일부 애널리틱스/OAuth 콜백 깨짐 가능. Mozilla/ACSC ISM-1788은 `strict-origin-when-cross-origin` 권장 → `presets.acsc()` 사용 | +| 7 | `Strict-Transport-Security` | `max-age=63072000; includeSubDomains` (2년) | Mozilla / RFC 6797. `preload` 토큰은 RFC 비포함, hstspreload.org 컨벤션 | +| 8 | `X-Content-Type-Options` | `nosniff` | OWASP | +| 9 | `X-DNS-Prefetch-Control` | `off` | OWASP. 보안보단 프라이버시/성능이나 OWASP 권장 유지 | +| 10 | `X-Frame-Options` | `deny` | OWASP/Mozilla 정규형(소문자). CSP `frame-ancestors 'none'`과 일치. **WAF 호환 주의**: Cloudflare/Akamai 일부 시그니처가 정확히 `DENY` 매칭 → `xFrameOptions: 'DENY'` 입력 시 그대로 송출하여 호환 | +| 11 | `X-Permitted-Cross-Domain-Policies` | `none` | OWASP. Flash/Silverlight EOL이나 PCI/Qualys 스캐너가 체크 | + +#### OWASP deviation 명시 + +| OWASP add.json | 본 라이브러리 | 사유 | +|---|---|---| +| `Cross-Origin-Embedder-Policy: require-corp` (default-add) | Default-OFF | 서드파티 임베드(이미지/폰트/CDN) 광범위 차단 위험. cross-origin isolation 필요 시 명시 활성. **Safari `credentialless` 미지원** | +| `Cache-Control: no-store, max-age=0` (default-add) | Default-OFF | 보안 비민감 응답까지 일괄 no-store 적용은 캐시 효율성 저하. 민감 라우트별 opt-in 또는 `presets.bsi()`/`presets.kisa()` 활성. PCI DSS §6.4.3 / KISA PIPA §7(6) 적용 라우트는 명시 활성 | +| `Clear-Site-Data` (default-add) | Default-OFF (action 헤더) | 모든 응답에 송출하면 매 요청마다 캐시/쿠키/스토리지 삭제 → 사용자 세션 파괴. 로그아웃 endpoint 전용 action 헤더로 opt-in | +| `interest-cohort` Permissions-Policy 항목 | 플랜 미포함 | FLoC 2022년 폐기, registry에서 완전 제거됨. OWASP `headers_add.json`에는 잔존(legacy noise) → 사용자가 `permissionsPolicy.features['interest-cohort']: []`로 명시 추가 가능 | + +#### CSP 기본값 설계 근거 + +OWASP 2026 공식 CSP를 기본값으로 채택. `default-src 'self'`에 의존하여 명시하지 않은 fetch 디렉티브(font-src, img-src, script-src, style-src 등)는 자동으로 `'self'`가 적용된다. + +| 디렉티브 | 값 | 근거 | +|---|---|---| +| `default-src` | `'self'` | OWASP 기본. font-src, img-src, script-src, style-src 등 미명시 fetch 디렉티브에 fallback | +| `base-uri` | `'self'` | base tag injection 방지 | +| `form-action` | `'self'` | 폼 제출 대상 제한 | +| `frame-ancestors` | `'none'` | OWASP 공식. X-Frame-Options `deny`와 일치 | +| `object-src` | `'none'` | Flash/Java 플러그인 완전 차단 | +| `manifest-src` | `'self'` | **PWA 호환 안전망**. CSP3 §6.6에 의해 `default-src`로 fallback **되지 않음** — 미명시 시 PWA `manifest.webmanifest` fetch 차단 (사일런트 깨짐). plan-extension (OWASP 미포함) | +| `upgrade-insecure-requests` | (boolean) | HTTP→HTTPS 자동 업그레이드. HSTS와 비충돌 (HSTS=호스트별, UIR=페이지별 navigations) — 둘 다 송출 권장 | + +> Google Fonts(`font-src 'self' https: data:`), 인라인 SVG(`img-src 'self' data:`) 등 외부 리소스가 필요한 경우 사용자가 디렉티브 override. + +### Default-OFF (opt-in, 22개) + +| # | 헤더 | 활성 시 기본값 | 이유 | +|---|---|---|---| +| 12 | `Cross-Origin-Embedder-Policy` | `require-corp` | OWASP 권장이나 서드파티 리소스 깨짐 위험으로 opt-in. cross-origin isolation(SharedArrayBuffer) 필요 시 활성. Safari `credentialless` 미지원 | +| 13 | `Content-Security-Policy-Report-Only` | 사용자 지정 | CSP 정책 모니터링 | +| 14 | `Cross-Origin-Opener-Policy-Report-Only` | 사용자 지정 | COOP 모니터링 | +| 15 | `Cross-Origin-Embedder-Policy-Report-Only` | 사용자 지정 | COEP 모니터링 | +| 16 | `Permissions-Policy-Report-Only` | 사용자 지정 | Permissions-Policy 모니터링 (Chromium 전용) | +| 17 | `Reporting-Endpoints` | 사용자 지정 | CSP/COOP/COEP/Integrity-Policy/NEL 위반 리포트 수신. RFC 9651 Dictionary. `default` endpoint는 fallback 컨벤션 | +| 18 | `Integrity-Policy` | `true` → `{ blockedDestinations: ['script', 'style'] }` | SRI 강제. RFC 9651 Dictionary, `sources` Inner List, `endpoints` Inner List of Token. Q1 2026 universal | +| 19 | `Integrity-Policy-Report-Only` | 사용자 지정 | SRI 모니터링 | +| 20 | `Clear-Site-Data` | `true` → `{ directives: ['cache', 'cookies', 'storage'] }` | 로그아웃 시 브라우저 데이터 삭제. W3C 표준 토큰: `cache`, `cookies`, `storage`, `executionContexts`, `clientHints`, `*`. `prefetchCache`/`prerenderCache`는 Chrome 비표준 | +| 21 | `Cache-Control` | `no-store, max-age=0` | OWASP. PCI DSS §6.4.3 / KISA PIPA §7(6) 적용 라우트는 활성. `pragma`/`expires`로 HTTP/1.0 호환 | +| 22 | `NEL` | 사용자 지정 | 네트워크 에러 로깅 (Chromium 전용). 레거시 `Report-To` 헤더에 의존 — NEL 활성 시 자동 생성 | +| 23 | `Document-Policy` | 사용자 지정 | WICG stalled 2022, Chromium 전용 실험 기능. RFC 9651 Dictionary | +| 24 | `Document-Policy-Report-Only` | 사용자 지정 | Document-Policy 모니터링 | +| 25 | `Require-Document-Policy` | 사용자 지정 | 중첩 콘텐츠 정책 요구 (실험적) | +| 26 | `Document-Isolation-Policy` | 사용자 지정 | 프레임별 격리 (Chrome 137+ stable). `isolate-and-require-corp` / `isolate-and-credentialless` / `none` | +| 27 | `Document-Isolation-Policy-Report-Only` | 사용자 지정 | Document-Isolation-Policy 모니터링 | +| 28 | `Timing-Allow-Origin` | 사용자 지정 | Resource Timing 사이드채널 방지 | +| 29 | `X-Robots-Tag` | `true` → `{ directives: ['noindex', 'nofollow'] }` | 민감 엔드포인트 크롤러 차단 | +| 30 | `X-Download-Options` | `noopen` | IE EOL(2022)이나 보안 스캐너(Qualys) 호환. v2.0(major) 제거 후보 | +| 31 | `X-XSS-Protection` | `0` | 레거시 XSS Auditor 명시 비활성. **OWASP 2026 `headers_add.json` 미포함**. CSP가 표준. 보안 스캐너 호환 opt-in. v2.0 제거 후보. **KISA 호환 모드**(`presets.kisa()`)는 `1; mode=block` 송출 — Korean public-sector 스캐너 대응 | +| 32 | `X-Permitted-Cross-Domain-Policies` (override) | `none` (Default-ON과 동일) | 사용자가 라우트별 `master-only`/`by-content-type`/`all` 설정 시 | +| 33 | `Server-Timing` (제거 권장 — 정보 제공) | — | OWASP cheat sheet 권장. 내부 타이밍 노출 위험. 본 라이브러리는 자동 송출 미실시, removeHeaders 기본 후보군에 포함 | + +### 기능 (헤더 제거, 1개) + +| # | 기능 | 기본 | 설명 | +|---|---|---|---| +| 34 | `removeHeaders` | OWASP must-strip 4종 + plan 확장 | 정보 노출 헤더 제거. `apply(response)`에서 동작 | + +#### removeHeaders 정책 + +- **기본값** (`removeHeaders: true` 또는 미지정): `Server`, `X-Powered-By`, `X-AspNet-Version`, `X-AspNetMvc-Version` (OWASP must-strip 4종) +- **`removeHeaders: 'owasp'`**: OWASP `headers_remove.json` 전체 70종 (자동 sync test 대상) +- **`removeHeaders: { headers: [...], additional: [...] }`**: 사용자 정의 +- 매칭: case-insensitive exact match. 와일드카드(`X-B3-*`, `X-Envoy-*`)는 constants에 known 이름 전개 +- **Server 헤더 제거 제약**: Bun.serve / Node http2가 미들웨어 이후 `Server`를 prepend하는 경우 `removeHeaders`로 못 막음. README에 런타임별 우회 레시피 명시 + +#### OWASP `headers_remove.json` 2026-03-05 (70종) + +`$wsep`, `Host-Header`, `K-Proxy-Request`, `Liferay-Portal`, `OracleCommerceCloud-Version`, `Pega-Host`, `Powered-By`, `Product`, `Server`, `Server-Timing`, `SourceMap`, `Via`, `X-AspNet-Version`, `X-AspNetMvc-Version`, `X-Atmosphere-error`, `X-Atmosphere-first-request`, `X-Atmosphere-tracking-id`, `X-B3-ParentSpanId`, `X-B3-Sampled`, `X-B3-SpanId`, `X-B3-TraceId`, `X-BEServer`, `X-Backside-Transport`, `X-CF-Powered-By`, `X-CMS`, `X-CalculatedBETarget`, `X-Cocoon-Version`, `X-Content-Encoded-By`, `X-DiagInfo`, `X-Envoy-Attempt-Count`, `X-Envoy-External-Address`, `X-Envoy-Internal`, `X-Envoy-Original-Dst-Host`, `X-Envoy-Upstream-Service-Time`, `X-FEServer`, `X-Framework`, `X-Generated-By`, `X-Generator`, `X-Jitsi-Release`, `X-Joomla-Version`, `X-Kubernetes-PF-FlowSchema-UI`, `X-Kubernetes-PF-PriorityLevel-UID`, `X-LiteSpeed-Cache`, `X-LiteSpeed-Cache-Control`, `X-LiteSpeed-Purge`, `X-LiteSpeed-Tag`, `X-LiteSpeed-Vary`, `X-Litespeed-Cache-Control`, `X-Mod-Pagespeed`, `X-Nextjs-Cache`, `X-Nextjs-Matched-Path`, `X-Nextjs-Page`, `X-Nextjs-Redirect`, `X-OWA-Version`, `X-Old-Content-Length`, `X-OneAgent-JS-Injection`, `X-Page-Speed`, `X-Php-Version`, `X-Powered-By`, `X-Powered-By-Plesk`, `X-Powered-CMS`, `X-Redirect-By`, `X-Runtime`, `X-Server-Powered-By`, `X-SourceFiles`, `X-SourceMap`, `X-Turbo-Charged-By`, `X-Umbraco-Version`, `X-Varnish`, `X-Varnish-Backend`, `X-Varnish-Cache`, `X-Varnish-Server`, `X-Woodpecker-Version`, `X-dtAgentId`, `X-dtHealthCheck`, `X-dtInjectedServlet`, `X-ruxit-JS-Agent` + +#### Plan 확장 (OWASP 미포함 trace ID) + +`X-Request-ID`, `X-Correlation-ID`, `X-Envoy-Decorator-Operation` — opt-in. + +## Permissions-Policy 피처 + +**중요**: Permissions-Policy HTTP 헤더는 Chromium + Firefox 지원, Safari 미지원 (~85% 글로벌). Tier 모델은 W3C registry 분류 + 브라우저 지원으로 재정의. + +### Registry ↔ 본 라이브러리 emit 매핑 (산정 표) + +W3C `webappsec-permissions-policy/features.md` registry 카운트와 본 라이브러리 emit 카운트를 reconcile. **L48 "Standardized 38 + Proposed 13 + Experimental 16 + Retired 2"**의 출처는 registry, **본 라이브러리 emit 53(Tier A 6 + B 35 + C 18) + Tier D off-list**의 출처는 본 라이브러리 union 타입. + +| registry 분류 | registry 카운트 | 본 라이브러리 처리 | emit 카운트 | 차이 사유 | +|---|---|---|---|---| +| **Standardized** | **38** | Tier A (6) + Tier B (35) + ch-ua-* 11종 의도적 제외 (Client Hints 영역) | 41 | 38 - 11 ch-ua + 14 = 41이 아니라, Standardized 38은 ch-ua 11 포함 → 38 - 11 = 27 emit. 본 라이브러리 Tier B 35는 27 (Standardized 비-ch-ua) + 8 (registry 외이지만 stable Standardized로 본 라이브러리가 분류한 항목 — `cross-origin-isolated`, `attribution-reporting` 등 일부 registry 분류와 본 라이브러리 분류 차) **검증 필요** | +| **Proposed** | **13** | Tier C에 9종 포함 (`gamepad`, `clipboard-read/write`, `speaker-selection`, `deferred-fetch`, `language-model`, `language-detector`, `summarizer`, `translator`, `writer`, `rewriter`, `autofill` 11종) | 11 | Proposed 13 중 2종(`manual-text` 등)은 보안성 낮음 → Tier D off-list | +| **Experimental** | **16** | Tier C에 7종 포함 (`local-fonts`, `unload`, `browsing-topics`, `captured-surface-control`, `smart-card`, `all-screens-capture`) | 6 | Experimental 16 중 OT 종료 미진입/Privacy Sandbox 광고 항목은 Tier D off-list | +| **Retired** | **2** | 의도적 제외 (`document-domain`, `window-placement`) | 0 | `window-management`로 rename된 신규 키만 Tier B emit | +| **off-registry** (Chromium intent / IWA 전용 / WICG draft) | — | Tier D — `(string & {})` union 폴백으로 사용자 명시 추가 가능, 본 라이브러리 default 미포함 | 0 | `controlled-frame`, `device-attributes`, `on-device-speech-recognition`, `deferred-fetch-minimal` 등 | + +> **registry 카운트 검증 절차**: `test/permissions-policy-registry-sync.test.ts`가 weekly `webappsec-permissions-policy/features.md` fetch → table 파싱 → 본 라이브러리 union과 diff. drift 발견 시 PR auto-create (OWASP sync 패턴 동일). 위 표의 "검증 필요" 항목은 첫 sync 실행 시 정확한 분포 확정. + +### Tier A (Universal — 모든 엔진 supported, 6) + +`*-credentials-get`/`*-credentials-create` 계열. Firefox/Chromium/Safari 모두 헤더 파싱 (실제 enforcement는 엔진별). + +| 피처 | 기본값 | 출처 | +|---|---|---| +| `publickey-credentials-get` | `()` | Standardized (registry) | +| `publickey-credentials-create` | `()` | Proposed (WebAuthn-3) | +| `identity-credentials-get` | `()` | Standardized | +| `digital-credentials-get` | `()` | Experimental | +| `digital-credentials-create` | `()` | Experimental | +| `otp-credentials` | `()` | Standardized | + +### Tier B (Chromium + Firefox parsed, 35) + +W3C registry **Standardized** 섹션에서 Universal 6종을 제외한 나머지. Firefox는 헤더를 받아들이며 일부 directive name을 인식. + +`accelerometer`, `ambient-light-sensor`, `attribution-reporting`, `autoplay`, `battery`, `bluetooth`, `camera`, `compute-pressure`, `cross-origin-isolated`, `direct-sockets`, `display-capture`, `encrypted-media`, `execution-while-not-rendered`, `execution-while-out-of-viewport`, `fullscreen`, `geolocation`, `gyroscope`, `hid`, `idle-detection`, `keyboard-map`, `magnetometer`, `mediasession`, `microphone`, `midi`, `navigation-override`, `payment`, `picture-in-picture`, `screen-wake-lock`, `serial`, `storage-access`, `sync-xhr`, `usb`, `web-share`, `window-management`, `xr-spatial-tracking` + +기본값: 모두 `()` (완전 비활성). 예외: `sync-xhr=(self)` (OWASP 공식값, 레거시 라이브러리 호환). + +> **registry 정정 (vs 기존 plan)**: `gamepad`는 W3C registry **Proposed** 섹션 — Tier C로 이동. `battery`/`direct-sockets`/`mediasession`/`navigation-override`/`execution-while-not-rendered`/`execution-while-out-of-viewport`는 registry **Standardized** — 기존 "unimplemented/cancelled" 사유로 제외했으나 잘못, Tier B에 포함. `direct-sockets`는 IWA 전용이지만 매우 강력(raw TCP/UDP)하므로 default-deny 가치 큼. + +### Tier C (Chromium-only stable, Proposed 13 + Experimental shipped 일부) + +W3C registry **Proposed** 섹션 + **Experimental**에서 Chromium stable shipped. + +| 피처 | 기본값 | registry | Chrome | 비고 | +|---|---|---|---|---| +| `gamepad` | `()` | Proposed | 86+ | (registry 정정으로 Tier C 이동) | +| `clipboard-read` | `()` | Proposed | 86+ | | +| `clipboard-write` | `()` | Proposed | 86+ | | +| `local-fonts` | `()` | Experimental | 103+ | Local Font Access API | +| `unload` | `()` | Experimental | 117+ | OWASP 2026 공식 포함 | +| `browsing-topics` | `()` | Experimental | 115+ | Topics API. **`interest-cohort` 후계자**, FLoC 2022 폐기 | +| `captured-surface-control` | `()` | Experimental | 122+ OT | Conditional Focus | +| `smart-card` | `()` | Experimental | 134+ OT | IWA | +| `speaker-selection` | `()` | Proposed | 미구현 | Speaker Selection API (selectAudioOutput) | +| `all-screens-capture` | `()` | Experimental | OT | Multi-screen capture | +| `deferred-fetch` | `()` | Proposed | 130+ | Fetch Later API. spec default `self` (5MB quota) | +| `language-model` | `()` | Proposed | 138+ | Built-in AI Prompt API | +| `language-detector` | `()` | Proposed | 138+ stable | Built-in AI Language Detector | +| `summarizer` | `()` | Proposed | 138+ stable | Built-in AI Summarizer | +| `translator` | `()` | Proposed | 138+ stable | Built-in AI Translator | +| `writer` | `()` | Proposed | 138+ | Built-in AI Writer | +| `rewriter` | `()` | Proposed | 138+ | Built-in AI Rewriter | +| `autofill` | `()` | Proposed | 미구현 | 인증/credential 보호 벡터 (embedded contexts) | + +### Tier D (Origin Trial / Experimental / off-registry, 신중 검토 후 선택 활성) + +기본 비포함 — 사용자가 features map에 직접 추가 (`PermissionsPolicyFeature` union의 `(string & {})` 폴백). + +- `controlled-frame` — IWA 전용, **W3C registry 외**, chromestatus만 +- `deferred-fetch-minimal` — MDN/chromestatus, registry 외(Issue #544) +- `device-attributes` — Chrome 신규 (Feb 2026 intent), registry 외 +- `on-device-speech-recognition` — Chrome 신규, registry 외 +- `manual-text` — Proposed (registry), 비보안 영역 +- `conversion-measurement`, `focus-without-user-activation`, `monetization`, `sync-script`, `vertical-scroll`, `trust-token-redemption`, `join-ad-interest-group`, `run-ad-auction` — Privacy Sandbox/광고/niche +- `private-state-token-issuance`, `private-state-token-redemption`, `shared-storage`, `shared-storage-select-url`, `private-aggregation` — Privacy Sandbox 광고 + +### 의도적 제외 + +| 항목 | 사유 | +|---|---| +| `interest-cohort` | FLoC 2022 폐기, registry에서 완전 삭제(Retired 표 아님). `browsing-topics`로 대체 | +| `document-domain` | W3C registry **Retired**. `document.domain` setter 자체 폐기 경로 | +| `window-placement` | **Retired**. `window-management`(Tier B)로 이름 변경 | +| `ch-ua-*` (11종) | Standardized이나 Client Hints 영역, `Accept-CH`/`Critical-CH`로 처리 | + +> **DX 참고**: 일반 사용자는 Default(Tier A+B 비활성)를 그대로 사용. 특정 피처만 허용: `permissionsPolicy: { features: { camera: ['self'], microphone: ['self'] } }`. 명시 안 한 피처는 기본값 `()` 유지. + +## 의도적 제외 헤더 + +| 제외 | 사유 | +|---|---| +| CORS 헤더 | `@zipbul/cors` 담당 | +| Access-Control-Allow-Private-Network | CORS 확장 → `@zipbul/cors` | +| Set-Cookie 속성 | 세션 미들웨어. 본 라이브러리 README는 RFC 6265bis-22 prefix(`__Host-`/`__Secure-`) 권장만 명시 | +| Sec-Fetch-* | 요청 헤더 | +| Client Hints (Accept-CH, Critical-CH 등) | 콘텐츠 협상/신뢰성, 보안 정책 아님 | +| WebSocket 헤더 | 프로토콜 레벨 | +| HTTP Signatures (RFC 9421) | API/서비스 간 통신, 브라우저 보안 아님 | +| Content-Digest / Repr-Digest (RFC 9530) | API 무결성 | +| Privacy Sandbox 광고 헤더 (Observe-Browsing-Topics 등) | 광고 측정, 본 라이브러리 범위 외 | +| Set-Login, IdP-SignIn-Status (FedCM) | IdP 전용, niche | +| Activate-Storage-Access | Storage Access API 응답 (action) | +| Service-Worker-Allowed, Service-Worker-Navigation-Preload | 앱 기능, 보안 아님 | +| Speculation-Rules | 성능, 보안 아님. CSP `'inline-speculation-rules'` 키워드만 본 라이브러리 `Csp` 상수에 포함 | +| Web Bundles / Signed Exchanges (Signature, Signed-Headers) | 패키징, 비표준 진행중 | +| No-Vary-Search | 캐시 키 | +| Use-As-Dictionary / Available-Dictionary (RFC 9842) | Compression Dictionary, same-origin 강제 | +| Alt-Svc / Alt-Used / Early-Data | 전송 인프라 | +| Content-Disposition | 라우트별 결정 | +| X-UA-Compatible | IE 폐기 | +| Supports-Loading-Mode | 성능 opt-in | +| Cookie-Indices (현재 미존재) / draft-ietf-httpbis-layered-cookies-01 | 쿠키 영역 | +| **Feature-Policy** | Permissions-Policy로 대체 | +| **Expect-CT** | 폐기 (Chrome 107) | +| **HPKP (Public-Key-Pins)** | 폐기 (Chrome 72, Firefox 72) | +| **DNT** | 폐기 (Firefox 135 토글 제거) | +| **Sec-GPC** | 요청 헤더 | +| **Origin-Isolation** | 폐기, `Origin-Agent-Cluster`로 대체 | +| **`Report-To` (legacy 헤더)** | Reporting API ED 2026-01-02에서 제거. 단 NEL이 여전히 의존 → NEL 활성 시 reporting 모듈에서 자동 생성 | +| Take It Down Act / OSA / OFAC 관련 | 표준 HTTP 헤더 미존재 | + +## 옵션 구조 + +각 헤더: `true`(기본값) / `false`(비활성) / 문자열 또는 객체(커스텀). 모든 `boolean | string` 타입에 `@defaultValue` JSDoc + `@example` + `@see` 필수. + +### CSP 디렉티브 커스터마이징 전략: Replace + +사용자가 특정 디렉티브를 설정하면 해당 디렉티브의 기본값을 **완전히 교체**(replace), 병합 안 함. + +```typescript +// CSP 기본값: default-src 'self'가 모든 fetch 디렉티브에 fallback +// Google Fonts 사용 시 fontSrc 명시 +Helmet.create({ + contentSecurityPolicy: { + directives: { fontSrc: [Csp.Self, 'https://fonts.gstatic.com'] } + } +}); + +// 인라인 SVG/data: URI 이미지 +Helmet.create({ + contentSecurityPolicy: { + directives: { imgSrc: [Csp.Self, 'data:'] } + } +}); +``` + +설정 안 한 디렉티브는 기본값 유지. 설정한 디렉티브만 교체. + +### CSP 직렬화 / 검증 규칙 + +- **직렬화**: 디렉티브명 lowercase, 디렉티브 구분자 `;` (trailing 없음), 소스 구분자 single ASCII space +- **소스 dedup**: 동일 디렉티브 내 중복 제거 (키워드/quoted-source case-sensitive, scheme/host case-insensitive) +- **빈 fetch directive 배열** (`scriptSrc: []`): validate 거부 — fetch는 최소 1개 소스 필요. `'none'` 명시 안내 +- **빈 sandbox 배열** (`sandbox: []`): bare `sandbox` 디렉티브로 직렬화 (모든 sandbox 토큰 적용 = 가장 제한적) +- **소스 표현 검증**: + - 키워드(`'self'`, `'none'`, `'unsafe-inline'`, ...) 따옴표 필수. 따옴표 없는 키워드(`self`, `none`) 감지 시 에러 + `Csp.Self` 사용 안내 + - scheme-source 정규식: `^[a-zA-Z][a-zA-Z0-9+\-.]*:$` (linear, ReDoS-safe) + - host-source 정규식: scheme(optional) + host(`*` | `*.\w+` | `\w+`) + port(optional) + path(optional). **CI gate**: `recheck` 또는 `safe-regex2`로 정규식 catastrophic backtracking 검증. 입력 길이 상한 2048자 + - **nonce 값** (CSP3 §2.3.1 nonce-source ABNF: `base64-value = 1*(ALPHA / DIGIT / "+" / "/" / "-" / "_") *2"="`): + - 정규식: `^[A-Za-z0-9+/_-]{16,256}={0,2}$` — `=`를 끝에만 허용 (mid-string 거부), 16자 하한(128bit) + 256자 상한(DoS 방지) + - base64-std(`+/`)와 base64url(`-_`) 혼용 거부 (단일 alphabet 강제) + - 거부 문자: `'`, `"`, `\`, `<`, `>`, `;`, ` `(0x20), 모든 C0 controls(U+0000-U+001F NUL/CR/LF 등), DEL(U+007F), Unicode whitespace(U+00A0 NBSP, U+2028 LINE-SEP, U+2029 PARA-SEP, U+FEFF BOM) + - hash 값 길이: sha256=44, sha384=64, sha512=88 (base64 with padding). 동일 charset/제어문자 정책 적용 + - **헤더 이름 정규화**: HTTP/2/3은 lowercase 강제. `applyHeadersTo()` / `apply()`는 emit 직전 `toLowerCase()` 보장 +- **상호 작용 경고** (warn, not error — `helmet.warnings`에 누적): + - `'unsafe-inline'` + nonce/hash 동시 → 브라우저가 nonce/hash 우선이라 `'unsafe-inline'` 무시 (CSP3 §6.7.3) + - `'unsafe-eval'` + `'wasm-unsafe-eval'` 동시 → 후자 redundant + - `default` Trusted Types policy-name 사용 → 모든 sink-side string에 적용되는 default policy 위험성 + - COOP `same-origin` + COEP **OFF** → `crossOriginIsolated` false. SAB/멀티스레드 WASM 미동작 + - Reporting-Endpoints `default` endpoint 누락 → CSP/COOP/COEP `report-to` 미지정 시 fallback 안 됨 + - Permissions-Policy features map 키가 `PermissionsPolicyFeature` union에 없으면 (registry 외 또는 오타) + - Clear-Site-Data `prefetchCache`/`prerenderCache` 비표준 토큰 + - sandbox 디렉티브 in Report-Only → 브라우저가 무시 (CSP3 명시) + - `'unsafe-allow-redirects'` 사용 → 종속 디렉티브 `navigate-to`가 CSP3에서 제거되어 effective dead grammar +- **frame-ancestors 제약**: `'unsafe-inline'`, `'unsafe-eval'`, `'strict-dynamic'`, `'unsafe-hashes'`, nonce, hash 거부 (CSP3 §6.4.2). `'self'`, `'none'`, scheme/host-source만 허용 +- **폐기 디렉티브 거부 (validate 에러)**: + - `prefetch-src` (Chromium 112 제거, CSP3 미포함) + - `plugin-types` (CSP3 제거) + - `block-all-mixed-content` (CSP3 deprecated, `upgrade-insecure-requests` 사용) + - `referrer` (CSP1, `Referrer-Policy` 헤더로 대체) + - `reflected-xss` (CSP1, legacy) + - `navigate-to` (CSP3 제거) +- **fetch fallback chain 검증** (CSP3 §6.1, §6.6): + - `script-src-elem`/`-attr` → `script-src` → `default-src` + - `frame-src` → `child-src` → `default-src` + - `worker-src` → `child-src` → `script-src` → `default-src` + - `style-src-elem`/`-attr` → `style-src` → `default-src` + - `manifest-src`는 `default-src`로 fallback 안 됨 — 사용자가 manifest fetch + default-src만 설정 시 warn +- **Report-Only 제약**: `sandbox` 디렉티브는 Report-Only에서 무시됨 → warn. `frame-ancestors`, `upgrade-insecure-requests`는 honor (CSP3) +- **report-to 값**: `Reporting-Endpoints` token name. 정규식 `^[A-Za-z0-9_-]+$`로 검증 +- **report-uri 값**: 공백 구분 URI 리스트 (CSP3 deprecated이나 사양 잔존). WHATWG URL 절대 URL 또는 path-relative URI 파싱 가능. 빈 문자열, 잘못된 URL, javascript: 등 비-fetch 스킴 거부 +- **Trusted Types** (W3C ED 2026-02-24 §4.2): + - `requireTrustedTypesFor`: `'script'` 토큰 1개만 + - `trustedTypes`: policy-name 정규식 `^[A-Za-z0-9\-#=_/@.%]+$` 또는 `'allow-duplicates'` / `'none'` / `*` + - **`default` 정책 이름**: 예약어. `createPolicy()` 시 이름 미지정 default policy. validator는 사용 시 warn (보안적으로 default 정책은 모든 sink-side string에 적용) + - 빈 `trustedTypes;` 디렉티브는 유효 (`'none'`과 동등) + - **report-to group name vs Trusted Types policy-name**: report-to group `^[A-Za-z0-9_-]+$`(짧은 token), policy-name `^[A-Za-z0-9\-#=_/@.%]+$`(특수문자 포함) — validator 분리 + - Firefox 148 stable shipped (2026-02-24, unflagged) — Chromium/Safari/Firefox 모두 enforcement +- **Hash Reporting** (CSP3 ED §2.3.1): `'report-sha256'`/`'-sha384'`/`'-sha512'` 키워드는 매칭이 아니라 매칭에 사용된 hash를 `report-to` endpoint에 송출하라는 지시. `Csp.ReportSha256` 등 상수로 노출 +- **Strict CSP 권장 패턴** (JSDoc 문서화, 자동 적용 X): + + ```typescript + scriptSrc: [Csp.nonce(nonce), Csp.StrictDynamic, "'unsafe-inline'", 'https:'] + // - 'nonce-X' + 'strict-dynamic': 모던 브라우저 + // - 'unsafe-inline': nonce/hash 미지원 구형 브라우저 fallback (모던에서는 nonce에 의해 무시) + // - https: : 'strict-dynamic' 미지원 구형 브라우저 fallback + ``` + +- **A11y / 접근성 영향**: `'unsafe-inline'` 미허용 + nonce/hash strict CSP는 일부 a11y 도구(axe-core inline injection, 스크린 리더 북마클릿, NVDA browse-mode) 동작을 방해할 수 있음. CI에서 a11y 테스트 시 `presets.api()` 또는 사용자 정의 CSP-Report-Only로 영향 측정 후 적용 권장 + +- **Default-src override 주의**: 사용자가 `scriptSrc`만 override 시 `default-src 'self'` fallback 적용 안 됨(스펙). nonce 사용 시 `scriptSrc`/`styleSrc` 둘 다 명시 권장 +- **Service Worker + CSP**: SW는 페이지 CSP를 약화/override 불가. SW 내부 fetched scripts는 SW 자체 CSP(SW script response time)에 종속 + +### CSP-violation report 형식 + +- **Legacy `application/csp-report`** (CSP2): kebab-case (`blocked-uri`, `violated-directive`, `original-policy`, `disposition`, `effective-directive`, `referrer`, `status-code`, `source-file`, `line-number`, `column-number`) +- **Reporting API `application/reports+json`** v1: camelCase (`blockedURL`, `effectiveDirective`, `disposition`, `documentURL`, `referrer`, `sample`, `statusCode`) +- `Helmet.parseCspReport(req)`는 양쪽 포맷을 정규화한 typed union 반환 +- **입력 검증 (DoS / pollution 방어)**: + - **Content-Type 화이트리스트**: `application/csp-report`, `application/reports+json`만 수락. 그 외 (`text/plain`, `application/json` 등)는 `HelmetError(UnsupportedCspReportContentType)` throw — Reporting API 스펙 §3.4 enforcement + - **본문 크기 상한**: 64KB. 초과 시 `HelmetError(CspReportTooLarge)` throw. `request.body` ReadableStream을 chunked read하며 누적 크기 추적 — 전체 buffer 후 크기 검사하면 DoS + - **단일 보고서 객체 수 상한**: `reports+json`은 배열 — 항목 ≤ 100. 초과 시 truncate + 첫 100개만 반환 (선택적으로 `TooManyReports` warning) + - **JSON parse 실패**: `HelmetError(InvalidCspReport)` — raw 입력 echo 금지 (`message`는 길이만 노출) + - **prototype pollution**: parsed 객체는 `Object.create(null)`로 재구성 후 known field만 복사. `__proto__`/`constructor`/`prototype` 키 무시 + - **URL 필드 sanitize**: `blockedURL`/`documentURL`/`source-file`은 WHATWG URL 파서 통과 시만 보존, 실패 시 raw string으로 보존하되 length ≤ 2048 truncate + - **타임아웃**: body read는 10초 timeout (`AbortController`로 wrap). 초과 시 `HelmetError(CspReportTimeout)` + - body는 1회 소비 — 호출자가 재사용 필요 시 `request.clone()` 후 전달 + +### Permissions-Policy 직렬화 규칙 + +- **RFC 9651 Structured Field Dictionary** 형식 +- 빈 features map (`{}`) → 헤더 송출 생략 (빈 SF Dictionary는 invalid) +- Allowlist 직렬화: + - `[]` → `feature=()` (none) + - `['*']` → `feature=*` (all, bare token) + - `['self']` → `feature=(self)` (bare token, **따옴표 없음**) + - `['self', 'https://x.com']` → `feature=(self "https://x.com")` (origin은 sf-string으로 double-quoted) +- 검증 (보안 강화): + - **WHATWG URL parse-and-reserialize 강제**: `new URL(input).origin` 추출 → `origin === "null"` 거부, scheme이 `https:`/`http:`이 아닌 경우 거부, userinfo/path/query/fragment 제거. 단순 regex/문자열 split 사용 금지 (sf-string injection 벡터) + - **sf-string escape 명시 (RFC 9651 §3.3.3)**: `"` → `\"`, `\` → `\\`. 다른 문자는 escape 미허용 (RFC 9651는 매우 제한적). 출력 문자열에 raw `"` / `\` 잔존 시 emit 거부 (assertion failure) + - `self`, `*`는 bare token으로만 송출. 다른 키워드는 spec 위반 → 에러 + - 잘못된 입력(`'self'` quoted, raw URL 등) 감지 시 자동 보정 + warn 또는 validate 에러 + - **map 키 prototype 보호**: `features` map은 내부적으로 `Object.create(null)` 또는 `Map` 사용. `__proto__`, `constructor`, `prototype` 키 거부 + `HelmetError(InvalidPermissionsPolicyToken)` + +### Nonce 주입 규칙 + +`headers({ nonce })` / `headersRecord({ nonce })` / `apply(response, { nonce })` 호출 시: + +- `script-src`에 `'nonce-{value}'` 주입 +- `style-src`에 `'nonce-{value}'` 주입 +- 사용자가 명시 설정한 `script-src-elem`, `style-src-elem`, `script-src-attr`, `style-src-attr`에도 주입 +- 명시 설정 안 한 `-elem`/`-attr` 변형에는 주입 X (fallback으로 `script-src`/`style-src` 적용) +- `Content-Security-Policy-Report-Only` 설정 시 동일 규칙 (모니터링 정확성) +- nonce 주입 시 CSP/CSP-RO만 재생성, 나머지 캐시 반환 + +### TypeScript 타입 + +```typescript +// Branded types (충돌 방지) +type Nonce = string & { readonly __brand: 'Nonce' }; +type EndpointName = string & { readonly __brand: 'EndpointName' }; +type HttpsUrl = `https://${string}`; + +/** + * resolveHelmetOptions 결과 — `HelmetOptions` 정규화 + 모든 default 채워짐 + deep-readonly. + * 내부 representation은 사용자 입력과 다름: + * - features map / endpoints map / policies map은 `Map<...>` 또는 `Object.create(null)` + * - boolean true/false는 명시 객체로 확장 + * - kebab-case 디렉티브 키로 정규화 (camelCase 입력 → kebab-case) + * - 사용자 미설정 디렉티브는 OWASP default로 채워짐 (CSP) + * helmet.toJSON()이 이 형태를 반환 (deep-readonly). + */ +type ResolvedHelmetOptions = DeepReadonly<{ + contentSecurityPolicy: ResolvedCspOptions | false; + contentSecurityPolicyReportOnly: ResolvedCspOptions | undefined; + crossOriginOpenerPolicy: CoopValue | false; + crossOriginOpenerPolicyReportOnly: CoopValue | undefined; + crossOriginEmbedderPolicy: CoepValue | false; + crossOriginEmbedderPolicyReportOnly: CoepValue | undefined; + crossOriginResourcePolicy: CorpValue | false; + originAgentCluster: boolean; + permissionsPolicy: ResolvedPermissionsPolicyOptions | false; + permissionsPolicyReportOnly: ResolvedPermissionsPolicyOptions | undefined; + referrerPolicy: ReferrerPolicyToken[] | false; + strictTransportSecurity: ResolvedHstsOptions | false; + xContentTypeOptions: boolean; + xDnsPrefetchControl: 'on' | 'off' | false; + xFrameOptions: 'deny' | 'sameorigin' | 'DENY' | 'SAMEORIGIN' | false; + xPermittedCrossDomainPolicies: 'none' | 'master-only' | 'by-content-type' | 'all' | false; + reportingEndpoints: ResolvedReportingEndpointsOptions | undefined; + integrityPolicy: ResolvedIntegrityPolicyOptions | false | undefined; + integrityPolicyReportOnly: ResolvedIntegrityPolicyOptions | undefined; + clearSiteData: ResolvedClearSiteDataOptions | false | undefined; + cacheControl: ResolvedCacheControlOptions | false | undefined; + nel: ResolvedNelOptions | undefined; + documentPolicy: ResolvedDocumentPolicyOptions | undefined; + documentPolicyReportOnly: ResolvedDocumentPolicyOptions | undefined; + requireDocumentPolicy: ResolvedDocumentPolicyOptions | undefined; + documentIsolationPolicy: 'isolate-and-require-corp' | 'isolate-and-credentialless' | 'none' | undefined; + documentIsolationPolicyReportOnly: 'isolate-and-require-corp' | 'isolate-and-credentialless' | 'none' | undefined; + timingAllowOrigin: string[] | undefined; + xRobotsTag: ResolvedXRobotsTagOptions | false | undefined; + xDownloadOptions: boolean; + xXssProtection: '0' | '1; mode=block' | false; + removeHeaders: ResolvedRemoveHeadersOptions; + messageFormatter: HelmetOptions['messageFormatter']; +}>; + +/** 깊은 readonly 변환 유틸 */ +type DeepReadonly = T extends (...args: any[]) => any + ? T + : T extends object + ? { readonly [K in keyof T]: DeepReadonly } + : T; + +interface HelmetOptions { + // ── Default-ON (11) ── + /** @defaultValue 위 CSP 기본값 설계 근거 참조 */ + contentSecurityPolicy?: boolean | ContentSecurityPolicyOptions; + /** @defaultValue 'same-origin' */ + crossOriginOpenerPolicy?: boolean | CoopValue; + /** @defaultValue 'same-origin' */ + crossOriginResourcePolicy?: boolean | CorpValue; + /** + * sf-boolean (RFC 9651). `true` → `?1` (origin-keyed agent cluster opt-in). + * `false` → `?0` (site-keyed opt-out). Chromium origin-keyed default 2025+. + * @defaultValue true (`?1`) + */ + originAgentCluster?: boolean; + /** @defaultValue Tier A+B 비활성, sync-xhr=(self) */ + permissionsPolicy?: boolean | PermissionsPolicyOptions; + /** @defaultValue 'no-referrer'. ACSC ISM-1788 호환은 `presets.acsc()` 사용 */ + referrerPolicy?: boolean | ReferrerPolicyToken | ReferrerPolicyToken[]; + /** @defaultValue { maxAge: 63072000, includeSubDomains: true } */ + strictTransportSecurity?: boolean | StrictTransportSecurityOptions; + /** @defaultValue 'nosniff' */ + xContentTypeOptions?: boolean; + /** @defaultValue 'off' */ + xDnsPrefetchControl?: boolean | 'on' | 'off'; + /** + * 입력은 case-insensitive, 출력은 입력 case 그대로 송출 (WAF 호환). + * @defaultValue 'deny' (소문자 OWASP 정규형) + */ + xFrameOptions?: boolean | 'deny' | 'sameorigin' | 'DENY' | 'SAMEORIGIN'; + /** @defaultValue 'none' */ + xPermittedCrossDomainPolicies?: boolean | 'none' | 'master-only' | 'by-content-type' | 'all'; + + // ── Default-OFF (opt-in, 22) ── + /** @defaultValue 'require-corp' (활성 시). Safari `credentialless` 미지원 */ + crossOriginEmbedderPolicy?: boolean | CoepValue; + contentSecurityPolicyReportOnly?: ContentSecurityPolicyOptions; + crossOriginOpenerPolicyReportOnly?: CoopValue; + crossOriginEmbedderPolicyReportOnly?: CoepValue; + permissionsPolicyReportOnly?: PermissionsPolicyOptions; + reportingEndpoints?: ReportingEndpointsOptions; + integrityPolicy?: boolean | IntegrityPolicyOptions; + integrityPolicyReportOnly?: IntegrityPolicyOptions; + clearSiteData?: boolean | ClearSiteDataOptions; + cacheControl?: boolean | CacheControlOptions; + nel?: NelOptions; + documentPolicy?: DocumentPolicyOptions; + documentPolicyReportOnly?: DocumentPolicyOptions; + requireDocumentPolicy?: DocumentPolicyOptions; + documentIsolationPolicy?: 'isolate-and-require-corp' | 'isolate-and-credentialless' | 'none'; + documentIsolationPolicyReportOnly?: 'isolate-and-require-corp' | 'isolate-and-credentialless' | 'none'; + timingAllowOrigin?: string | string[]; + xRobotsTag?: boolean | XRobotsTagOptions; + /** @defaultValue 'noopen' (IE EOL — v2.0 제거 후보) */ + xDownloadOptions?: boolean; + /** @defaultValue '0'. KISA 호환은 `presets.kisa()` ('1; mode=block') */ + xXssProtection?: boolean | '0' | '1; mode=block'; + + // ── 헤더 제거 ── + removeHeaders?: boolean | 'owasp' | RemoveHeadersOptions; + + // ── i18n 훅 (deferred contract) ── + /** + * 검증 메시지 포매터. ESL 사용자 / 다국어 필요 시 활용. + * - 미설정: 영문 default message + reason enum + * - 설정: `(reason, ctx) => translatedMessage` — reason enum이 i18n 키 역할 + * 라이브러리 자체는 i18n 파이프라인 미내장. + * + * **호출 시멘틱 (보안)**: + * - 사용자 함수는 `try/catch`로 wrap. throw 시 영문 default 메시지로 fallback + `HelmetWarning(MessageFormatterFailed)` 누적 + * - 반환값이 `string`이 아니면 (undefined / null / 객체) 동일 fallback + * - validate 자체는 fallback 후 정상 진행 — 사용자 함수 결함이 라이브러리 검증을 마비시키지 않음 + * - **사용자 함수에 raw user input은 전달 안 함** — `context.path`는 구조 경로(`scriptSrc[2]`), `context.meta`는 길이/인덱스만. echo injection 방지 + * - 동기 함수만 허용 — Promise 반환 시 fallback (validate 동기 보장 위해) + * - 호출 횟수 상한: violation 1건당 1회. messageFormatter가 의도적으로 felony loop를 만들 수 없음 + */ + messageFormatter?: (reason: HelmetErrorReason | HelmetWarningReason, context: { path: string; meta?: unknown }) => string; +} + +// 공통 sub-union 추출 (DRY) +type CoopValue = 'same-origin' | 'same-origin-allow-popups' | 'noopener-allow-popups' | 'unsafe-none'; +type CorpValue = 'same-origin' | 'same-site' | 'cross-origin'; +type CoepValue = 'require-corp' | 'credentialless' | 'unsafe-none'; +``` + +### 서브 옵션 인터페이스 + +```typescript +interface ContentSecurityPolicyOptions { + directives?: CspDirectives; +} + +interface CspDirectives { + // ── Fetch 디렉티브 (17) ── + defaultSrc?: CspSource[]; + childSrc?: CspSource[]; + connectSrc?: CspSource[]; + fencedFrameSrc?: CspSource[]; // WICG Fenced Frame draft (CSP3 ED 외) + fontSrc?: CspSource[]; + frameSrc?: CspSource[]; + imgSrc?: CspSource[]; + manifestSrc?: CspSource[]; // default-src로 fallback 안 됨 + mediaSrc?: CspSource[]; + objectSrc?: CspSource[]; + scriptSrc?: CspSource[]; + scriptSrcAttr?: CspSource[]; + scriptSrcElem?: CspSource[]; + styleSrc?: CspSource[]; + styleSrcAttr?: CspSource[]; + styleSrcElem?: CspSource[]; + workerSrc?: CspSource[]; + + // ── Document (2) ── + baseUri?: CspSource[]; + sandbox?: SandboxToken[]; + + // ── Navigation (2) ── + formAction?: CspSource[]; + frameAncestors?: CspSource[]; + + // ── Reporting (2) ── + reportTo?: string; // Reporting-Endpoints 키 이름 (URL 아님) + reportUri?: string; // CSP3 deprecated이나 잔존 (Safari 일부 의존) + + // ── WebRTC (1, CSP3 신규) ── + webrtc?: 'allow' | 'block'; + + // ── 기타 ── + upgradeInsecureRequests?: boolean; + /** Trusted Types W3C ED §4.2 */ + requireTrustedTypesFor?: TrustedTypesRequireToken[]; + /** Trusted Types W3C ED §4.2 (policy-name regex 별도) */ + trustedTypes?: TrustedTypesToken[]; +} + +/** + * CSP3 ED §2.3.1 keyword-source ABNF + URL/scheme/host expressions. + * Template literal union으로 컴파일 타임에 따옴표 누락 + scheme/host 형태 강제. + */ +type CspKeywordSource = + | "'self'" | "'none'" | "'unsafe-inline'" | "'unsafe-eval'" + | "'strict-dynamic'" | "'unsafe-hashes'" | "'report-sample'" + | "'wasm-unsafe-eval'" | "'inline-speculation-rules'" + | "'unsafe-webtransport-hashes'" + | "'report-sha256'" | "'report-sha384'" | "'report-sha512'"; +type CspNonceSource = `'nonce-${string}'`; +type CspHashSource = `'sha256-${string}'` | `'sha384-${string}'` | `'sha512-${string}'`; +type CspSchemeSource = `${string}:`; +type CspHostSource = `https://${string}` | `http://${string}` | `wss://${string}` | `ws://${string}`; +type CspSource = + | CspKeywordSource + | CspNonceSource + | CspHashSource + | CspSchemeSource + | CspHostSource + | '*' + | (string & {}); + +type TrustedTypesRequireToken = "'script'"; +type TrustedTypesToken = "'allow-duplicates'" | "'none'" | '*' | (string & {}); + +/** + * HTML Standard §iframe sandboxing 13 토큰 (canonical) + Storage Access API 확장 1 토큰 = 14 허용. + * CSP3 ED §6.3.2 sandbox는 HTML iframe sandbox 토큰 집합을 그대로 참조. + * `allow-storage-access-by-user-activation`은 HTML core 외, Storage Access API 스펙 확장 (Chromium/Safari 지원). + */ +type SandboxToken = + | 'allow-downloads' + | 'allow-forms' + | 'allow-modals' + | 'allow-orientation-lock' + | 'allow-pointer-lock' + | 'allow-popups' + | 'allow-popups-to-escape-sandbox' + | 'allow-presentation' + | 'allow-same-origin' + | 'allow-scripts' + | 'allow-storage-access-by-user-activation' // Storage Access API extension + | 'allow-top-navigation' + | 'allow-top-navigation-by-user-activation' + | 'allow-top-navigation-to-custom-protocols'; + +interface StrictTransportSecurityOptions { + /** default: 63072000 (2년, Mozilla 권장) */ + maxAge?: number; + /** default: true */ + includeSubDomains?: boolean; + /** + * default: false. **RFC 6797 비표준** (hstspreload.org 컨벤션). + * `true` 설정 시 validate가 hstspreload.org 요구사항 강제: + * - `maxAge >= 31536000` (1년) + * - `includeSubDomains: true` + * - 도메인은 base domain (apex)에서만 송출 권장 + * 위반 시 `HelmetError` (HstsPreloadRequirementMissing). + */ + preload?: boolean; +} + +type PermissionsPolicyFeature = + // ── Tier A (Universal, 6) ── + | 'publickey-credentials-get' | 'publickey-credentials-create' + | 'identity-credentials-get' | 'digital-credentials-get' | 'digital-credentials-create' + | 'otp-credentials' + // ── Tier B (Chromium + Firefox parsed, 35; W3C registry Standardized 제외 Tier A 6종) ── + | 'accelerometer' | 'ambient-light-sensor' | 'attribution-reporting' | 'autoplay' + | 'battery' | 'bluetooth' | 'camera' | 'compute-pressure' | 'cross-origin-isolated' + | 'direct-sockets' | 'display-capture' | 'encrypted-media' + | 'execution-while-not-rendered' | 'execution-while-out-of-viewport' + | 'fullscreen' | 'geolocation' | 'gyroscope' | 'hid' | 'idle-detection' + | 'keyboard-map' | 'magnetometer' | 'mediasession' | 'microphone' | 'midi' + | 'navigation-override' | 'payment' | 'picture-in-picture' | 'screen-wake-lock' + | 'serial' | 'storage-access' | 'sync-xhr' | 'usb' | 'web-share' + | 'window-management' | 'xr-spatial-tracking' + // ── Tier C (Chromium-only stable, 18) ── + | 'gamepad' | 'clipboard-read' | 'clipboard-write' | 'local-fonts' + | 'unload' | 'browsing-topics' + | 'captured-surface-control' | 'smart-card' | 'speaker-selection' + | 'all-screens-capture' | 'deferred-fetch' + | 'language-model' | 'language-detector' | 'summarizer' | 'translator' + | 'writer' | 'rewriter' | 'autofill'; + +type PermissionsPolicyAllowlist = '*' | 'self' | HttpsUrl | (string & {}); + +interface PermissionsPolicyOptions { + /** + * features map. 입력은 `Record`이지만 내부 저장은 `Map` — prototype pollution 방어. + * 검증: + * - `__proto__`, `constructor`, `prototype` 키 거부 (`HelmetError(InvalidPermissionsPolicyToken)`) + * - feature name 길이 상한 64자, allowlist 항목 수 상한 32개 (header bloat 방지) + * - allowlist origin은 WHATWG URL `new URL(input).origin` reserialize → `origin === "null"` / non-https 거부 + */ + features?: Partial>; +} + +interface ReportingEndpointsOptions { + /** + * RFC 9651 Structured Field Dictionary. URL은 sf-string 따옴표. + * 직렬화: `Reporting-Endpoints: default="https://example.com/reports", csp-endpoint="https://example.com/csp"` + * `default` 키는 Reporting API fallback 컨벤션. + * 검증: + * - 모든 URL은 HTTPS 강제 (HTTP 거부, validate 에러) + * - URL은 WHATWG URL `new URL(...)` 파싱 후 `.toString()` reserialize (raw 문자열 사용 안 함) + * - endpoint name 정규식: `^[A-Za-z0-9_-]{1,64}$` — 길이 상한 64자 + * - 내부 저장: `Map` (prototype pollution 방어) + * - `__proto__`, `constructor`, `prototype` 키 거부 + * - 전체 endpoints 수 상한: 32개 (header bloat + DoS 방지) + */ + endpoints: Record; +} + +interface IntegrityPolicyOptions { + /** + * RFC 9651 Structured Header Dictionary. 멤버는 Inner List of Token. + * 직렬화: `blocked-destinations=(script style), sources=(inline), endpoints=(default csp-endpoint)` + */ + blockedDestinations?: ('script' | 'style')[]; + /** SRI 적용 대상 소스. 현재 `inline`만 유효. 미명시 시 spec 기본값 `(inline)` 자동 적용 */ + sources?: ('inline')[]; + /** Reporting-Endpoints에 정의된 endpoint name(s). 복수 허용 (sf Inner List) */ + endpoints?: string[]; +} + +interface ClearSiteDataOptions { + /** + * W3C 표준 토큰: `cache`, `cookies`, `storage`, `executionContexts`, `clientHints`, `*`. + * `prefetchCache`, `prerenderCache`는 Chrome 비표준 확장 — validate 경고 (에러 아님). + * 직렬화 시 토큰은 sf-string 따옴표 (RFC 9651 List of sf-string). + */ + directives?: ('cache' | 'cookies' | 'storage' | 'executionContexts' | 'clientHints' | 'prefetchCache' | 'prerenderCache' | '*')[]; +} + +interface CacheControlOptions { + /** + * default: `'no-store, max-age=0'` (OWASP). + * RFC 9111 §5.2.2.5: `no-store` 단독 충분. OWASP 공식값 유지하되 사용자 단순화 가능 + */ + value?: string; + /** HTTP/1.0 호환: Pragma: no-cache. HTTP/1.1+에서는 무시됨 */ + pragma?: boolean; + /** HTTP/1.0 호환: Expires: 0. HTTP/1.1+에서는 Cache-Control 우선 */ + expires?: boolean; +} + +interface NelOptions { + /** Reporting-Endpoints에 정의된 endpoint name. validate에서 존재 검증 + Report-To 자동 생성 */ + reportTo: string; + /** 정책 적용 기간 (초) */ + maxAge: number; + includeSubdomains?: boolean; + /** + * 성공 응답 sampling rate (0.0–1.0). 프로덕션 권장: 0.1 (10%). + * 미명시 시 spec default(0.0, 성공 미리포트) + */ + successFraction?: number; + /** + * 실패 응답 sampling rate (0.0–1.0). 프로덕션 권장: 1.0 (모든 실패 분석 가치) + */ + failureFraction?: number; +} + +/** + * NEL/Reporting 운영 가이드 (Reporting API L1 §4.1): + * - reporting endpoint URL은 same-origin이거나 CORS-enabled 필수 + * - Reporting-Endpoints 헤더의 모든 endpoint URL은 HTTPS 필수 + * - NEL은 Chromium 전용 (Firefox/Safari 미수신) + * - Reporting-Endpoints 본체는 Chromium + Firefox 149+ (2026-03-24) + */ + +interface DocumentPolicyOptions { + /** + * RFC 9651 Dictionary. 값은 sf-item: boolean / integer / decimal / string / token / inner-list. + * 예: `{ 'document-write': false, 'js-profiling': true, 'force-load-at-top': false }` + * + * 보안: 내부 저장은 `Map` (prototype pollution 방어). + * `__proto__`, `constructor`, `prototype` 키 거부. policies 수 상한 64개 + */ + policies: Record; +} + +interface XRobotsTagOptions { + directives?: string[]; +} + +interface RemoveHeadersOptions { + /** + * 제거할 헤더 목록 (사용자가 전체 제어 시). + * 기본: `['Server', 'X-Powered-By', 'X-AspNet-Version', 'X-AspNetMvc-Version']` (must-strip 4종) + * 입력은 case-insensitive — 라이브러리가 lowercase로 정규화. + */ + headers?: string[]; + /** + * 기본 목록에 추가로 제거할 헤더 (병합). + * `removeHeaders: 'owasp'`로 `headers_remove.json` 70종 프리셋 사용. + */ + additional?: string[]; +} + +type ReferrerPolicyToken = + | 'no-referrer' + | 'no-referrer-when-downgrade' + | 'origin' + | 'origin-when-cross-origin' + | 'same-origin' + | 'strict-origin' + | 'strict-origin-when-cross-origin' + | 'unsafe-url'; + +// ── CSP 키워드 상수 (타입 안전성) ── +// 키워드 따옴표 누락은 보안 구멍 — validate에서 감지 시 에러. +// CSP3 ED §2.3.1 keyword-source 전수 반영. +const Csp = { + // 표준 키워드 (CSP3 ED §2.3.1) + Self: "'self'", + None: "'none'", + UnsafeInline: "'unsafe-inline'", + UnsafeEval: "'unsafe-eval'", + UnsafeHashes: "'unsafe-hashes'", + StrictDynamic: "'strict-dynamic'", + ReportSample: "'report-sample'", + WasmUnsafeEval: "'wasm-unsafe-eval'", + // WebTransport 통합 (CSP3 ED §2.3.1) + UnsafeWebTransportHashes: "'unsafe-webtransport-hashes'", + // Speculation Rules — CSP3는 keyword-source 확장 지점만 정의, 토큰 자체는 Speculation Rules / HTML Standard + InlineSpeculationRules: "'inline-speculation-rules'", + // CSP Hash Reporting (CSP3 ED §2.3.1) — 매칭이 아니라 사용된 hash를 report-to endpoint에 송출 지시 + ReportSha256: "'report-sha256'", + ReportSha384: "'report-sha384'", + ReportSha512: "'report-sha512'", + // 동적 + nonce: (value: string) => `'nonce-${value}'` as `'nonce-${string}'`, + hash: (algo: 'sha256' | 'sha384' | 'sha512', value: string) => + `'${algo}-${value}'` as `'${typeof algo}-${string}'`, +} as const; + +// 의도적 미포함 (출처 검증 후 제외): +// - 'unsafe-allow-redirects' — keyword-source 문법 잔존하나 종속 디렉티브 `navigate-to`가 CSP3 제거 → effective dead grammar +// - 'trusted-types-eval' — keyword-source의 일부이나 Trusted Types 스펙은 별도 메커니즘. 현재 브라우저 enforcement 없음 + +// 사용 예: +// scriptSrc: [Csp.Self, Csp.StrictDynamic] +// scriptSrc: [Csp.Self, Csp.nonce('abc123')] +// scriptSrc: [Csp.Self, Csp.InlineSpeculationRules] // +``` + +`crypto.randomUUID()` 비권장 — 122bit로 CSP3 §8 미달. + +### Cookie 보안 (범위 외, 권장) + +`Set-Cookie`는 본 라이브러리 미관여. draft-ietf-httpbis-rfc6265bis-22 prefix 권장: + +- `__Host-`: `Secure` + `Path=/` + Domain 미지정 → 서브도메인 cookie injection 방지 +- `__Secure-`: `Secure` 강제 + +`SameSite=Lax`/`Strict`, `HttpOnly`, `Secure` 조합을 세션 cookie에 적용. + +### SRI 사용 (Integrity-Policy + 빌드 통합) + +신규 콘텐츠는 **sha384** 권장 (W3C SRI Level 2 FPWD 2025-04-22 — CR 미진입 ED). sha256은 호환 유지. + +```html + +``` + +빌드 통합 — Vite/Rollup/webpack에서 번들 hash 추출: + +```typescript +const hash = await Helmet.hashFromString(scriptText, 'sha384'); +const helmet = Helmet.create({ + contentSecurityPolicy: { + directives: { scriptSrc: [Csp.Self, Csp.hash('sha384', hash)] } + } +}); +``` + +**알고리즘 매트릭스** (CSP3 ED §2.3.1 hash-algorithm + Web Crypto subtle.digest 런타임 지원): + +| 알고리즘 | CSP3 spec | Bun | Node | Workers | Deno | Safari/Web | 권장 | +|---|---|---|---|---|---|---|---| +| SHA-256 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 호환 | +| SHA-384 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | **2026 권장** | +| SHA-512 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 고가치 | +| SHA-3 / Keccak | ✗ (CSP3 미정의) | ✗ | ✗ | ✗ | ✗ | ✗ | 미지원 | +| ML-DSA / SLH-DSA (PQ) | ✗ (2026 미정) | ✗ | ✗ | ✗ | ✗ | ✗ | 미지원 | + +`Csp.hash()`는 `'sha256' | 'sha384' | 'sha512'` 유니온만 허용. SHA-3 / 포스트퀀텀은 CSP3 spec 미정의 — 향후 W3C 결정 시 minor. + +`Helmet.hashFromString()` 입력 타입: `string | ArrayBuffer | Uint8Array | Blob | ReadableStream`. 파일 hash가 필요한 경우 사용자가 런타임별로 입력 변환 (Bun: `await Bun.file(path).arrayBuffer()`, Node: `await fs.readFile(path)`, Workers: `await fetch(url).then(r => r.arrayBuffer())`). + +**SubtleCrypto incremental API 부재 트레이드오프**: Web Crypto `subtle.digest`는 chunked/incremental 입력을 지원하지 않음 — 입력 전체를 buffer에 누적 후 단일 호출. `ReadableStream` 입력 시 라이브러리는 내부적으로 `for await (const chunk of stream)`로 Uint8Array에 concat하여 전달. 매우 큰 파일(>100MB)에는 메모리 비용 → 빌드 도구(Vite/Rollup)에서 파일별 분할 hash 권장. JS native SHA-2 폴백은 의도적으로 미제공 (의존성 0 유지). + +### 핫패스 최적화 + +요청당 `headers()` 할당 부담 시 `headersRecord()` (Hono `c.header(name, value)` 루프) 또는 `applyHeadersTo()` (in-place): + +```typescript +const record = helmet.headersRecord({ nonce }); +for (const [name, value] of Object.entries(record)) c.header(name, value); + +// 또는 in-place +helmet.applyHeadersTo(c.res.headers, { nonce }); +``` + +### Cross-origin isolation (SharedArrayBuffer) + +`crossOriginIsolated` 활성화 조건 (모두 충족): + +1. COOP `same-origin` (Default-ON ✓) +2. COEP `require-corp` 또는 `credentialless` (**Default-OFF** — 명시 활성 필요) +3. 또는 Document-Isolation-Policy `isolate-and-require-corp` / `isolate-and-credentialless` (Chromium 전용, Chrome 137+ stable) + +```typescript +Helmet.create({ crossOriginEmbedderPolicy: 'require-corp' }); +``` + +COOP `same-origin` + COEP OFF 시 `helmet.warnings`에 `CoopWithoutCoep` 누적. + +### Server 헤더 제거 제약 + +런타임이 미들웨어 이후 `Server`를 prepend하는 경우 `removeHeaders`로 못 막음: + +- **Bun.serve**: `serverHeader` 옵션으로 비활성 (`Bun.serve({ ..., serverHeader: '' })`) +- **Node http/http2**: 응답 후 `res.removeHeader('Server')` 또는 reverse proxy(nginx `server_tokens off;`) 우회 +- **Cloudflare Workers / Vercel Edge**: 플랫폼 자동 주입 (사용자 제어 불가) + +### WAF 호환 + +Cloudflare/Akamai 일부 시그니처가 `X-Frame-Options: DENY`(uppercase) 정확 매칭. `xFrameOptions: 'DENY'` 입력 시 입력 case 그대로 송출 (lowercase 정규화 안 함). + +### 응답 헤더 바이트 예산 + +기본 활성 헤더 + 큰 CSP + OWASP removeHeaders 프리셋 시 응답 헤더 합계 4–8KB 도달 가능: + +- Bun.serve: 64KB (기본) +- Node http: 16KB (--max-http-header-size로 조정) +- nginx `large_client_header_buffers`: 기본 4×8KB +- Cloudflare: 32KB (Worker 응답) +- AWS API Gateway: 10KB + +해결: CSP `frame-ancestors 'none'` 사용 시 X-Frame-Options 생략 가능 / Permissions-Policy 미사용 피처는 명시 안 함 / removeHeaders는 실제로 노출되는 헤더만. + +### 런타임 호환성 + +코어 모듈은 `globalThis.crypto`, `Headers`, `Response`만 사용 — 런타임 비종속: + +- Bun 1.x ✓ (primary target) +- Cloudflare Workers ✓ (compatibility_date `2024-09-23` 이상, `streams_enable_constructors` 권장) +- Deno 1.40+ / Deno Deploy ✓ +- Vercel Edge Runtime ✓ +- Node.js 19+ ✓ + +`Bun.*` API 미사용. 통합 예제만 Bun 우선 표기. + +### Cloudflare 운영 가이드 + +**Transform Rules 우선, Workers는 동적 정책 전용.** 정적 헤더 (Default-ON 11종 등)는 [Cloudflare Modify Response Header Transform Rules](https://developers.cloudflare.com/rules/transform/response-header-modification/)에서 엣지 캐시 레이어에 송출 — 컴퓨트 비용 0. Worker는 다음에만 사용: + +- 요청별 nonce가 필요한 CSP (SSR, SPA hydration) +- 테넌트별/A-B별 CSP-Report-Only +- Reporting-Endpoints 토큰 동적 회전 + +**Worker `Response.headers` immutability**: `fetch()`로 받은 Response의 헤더는 immutable. `applyHeadersTo(fetched.headers)`는 `TypeError: immutable` throw. 반드시 `helmet.apply(fetched)` 또는 `new Response(fetched.body, { headers: new Headers(fetched.headers) })`로 클론 후 사용. + +**Pages Functions vs Workers**: Pages는 `_headers` 파일이 있고 `cf-*` 자동 주입. `_headers`와 Helmet 출력 충돌 시 Pages가 prepend → `_headers`에서 동일 헤더 미설정 권장. + +**번들 사이즈 한도**: Worker 1MB compressed (Free: 1MB, Paid: 10MB). `@zipbul/helmet/csp` + `@zipbul/helmet/hsts`만 import 시 코어 < 8KB gzipped 목표. + +### 미래 헤더 watchlist (현재 미배출) + +다음 헤더는 표준 진행 중이나 본 라이브러리는 stable 진입까지 미배출: + +- **Connection-Allowlists** (Chrome OT 148-151) — deny-by-default 네트워크 방화벽 응답 헤더. OT 종료 후 stable 도달 시 minor 추가 +- **Sec-Fetch-Storage-Access** / **Activate-Storage-Access** (Chrome 133+ shipped) — Storage Access API. 동작 헤더이며 본 라이브러리 보안 정책 범위 외 (참고: Plan §의도적 제외 표 참조) +- **HTTPS-Upgrades / HTTPS-First** (Chrome 147 ESB, Chrome 154 default 2026-Q4) — 브라우저 기능. **HSTS는 여전히 필수** (HTTPS-Upgrades는 fallback 레이어, HSTS 대체 아님). 본 라이브러리 변경 없음 +- **Signature-based SRI Ed25519** — `'ed25519-...'` integrity prefix. SRI L2 CR 진입 + 브라우저 stable 시 `Csp.hash` 알고리즘 union 확장 + +### Vercel 운영 가이드 + +- **헤더 적용 순서**: `next.config.js headers()` → middleware.ts → Route Handler → Helmet. **Vercel은 `next.config.js`를 마지막에 적용**할 수 있어 Helmet 출력을 clobber. Helmet 사용 시 `next.config.js headers()` 제거 또는 보조 사용 +- Edge Functions vs Node Functions: Headers API parity 보장. `runtime: 'edge'` export 시 `globalThis.crypto` 사용 가능 +- `x-vercel-*` 헤더(`x-vercel-cache`, `x-vercel-id`, `x-vercel-edge-region` 등)는 `removeHeaders` 후보군에 추가 권장 (plan-extension) + +### Deno / JSR + +본 라이브러리는 **npm + JSR 양쪽 게시** 검토. `@zipbul/helmet`은 npm, `@zipbul/helmet`은 JSR(jsr.io). JSR slow types 정책에 부합 — 모든 export에 명시 타입 (no inferred types in public API). + +Deno 1.40+ `Headers.delete()`는 `Set-Cookie` 다중 값을 모두 삭제 (Deno 1.40 미만은 첫 항목만). `apply()` 알고리즘은 `getSetCookie()` + `append`로 다중 값 명시 보존 → 안전. + +### Bun 운영 가이드 + +```typescript +const helmet = Helmet.create(); + +Bun.serve({ + serverHeader: '', + async fetch(req) { + const url = new URL(req.url); + if (url.pathname.startsWith('/static/')) { + // Bun.file() static serving — Helmet 적용 + const file = Bun.file(`./public${url.pathname}`); + const res = new Response(file); + return helmet.apply(res); + } + return helmet.apply(new Response('hello')); + }, +}); +``` + +Bun dev hot-reload 시 헤더 캐시는 모듈 재로드와 함께 재생성 → 일관성 유지. + +## 프레임워크 통합 + +### Hono (Bun) + +```typescript +import { Hono } from 'hono'; +import { Helmet } from '@zipbul/helmet'; + +const helmet = Helmet.create(); +const app = new Hono(); + +app.use(async (c, next) => { + await next(); + const nonce = Helmet.generateNonce(); + c.set('nonce', nonce); // 템플릿에서 c.get('nonce') 사용 + helmet.applyHeadersTo(c.res.headers, { nonce }); +}); +``` + +### Elysia (Bun) + +```typescript +import { Elysia } from 'elysia'; +import { Helmet } from '@zipbul/helmet'; + +const helmet = Helmet.create(); +new Elysia() + .onAfterHandle(({ response }) => + response instanceof Response ? helmet.apply(response) : response + ); +``` + +### `Bun.serve` (raw) + +```typescript +const helmet = Helmet.create(); +Bun.serve({ + serverHeader: '', // Server 헤더 제거 + fetch(req) { + return helmet.apply(new Response('hello')); + }, +}); +``` + +### Express (Node, 어댑터 필요) + +Web `Response` 기반이므로 Express(Node `res`)와 직접 호환 안 됨. Hono 어댑터(`@hono/node-server`) 사용 권장. + +### Next.js (App Router, `@zipbul/helmet/next`) + +```typescript +// middleware.ts +import { NextResponse } from 'next/server'; +import { Helmet } from '@zipbul/helmet'; +import { withNonce } from '@zipbul/helmet/next'; + +const helmet = Helmet.create(); + +export function middleware(request: Request) { + const nonce = Helmet.generateNonce(); + const response = NextResponse.next({ + request: { headers: withNonce(request.headers, nonce) }, // x-nonce 헤더로 RSC에 전달 + }); + helmet.applyHeadersTo(response.headers, { nonce }); + return response; +} + +// app/layout.tsx +import { headers } from 'next/headers'; +const nonce = (await headers()).get('x-nonce') ?? ''; +return