Skip to content

feat(cookie): RFC 6265bis cookie middleware#48

Merged
parkrevil merged 7 commits into
mainfrom
feat/cookie
May 11, 2026
Merged

feat(cookie): RFC 6265bis cookie middleware#48
parkrevil merged 7 commits into
mainfrom
feat/cookie

Conversation

@parkrevil

Copy link
Copy Markdown
Contributor

Cookie middleware package with RFC 6265bis conformance and security hardening.

parkrevil and others added 7 commits March 21, 2026 22:53
RFC 6265/6265bis compliant cookie middleware with:
- HMAC signing (sha256/sha384/sha512) with key rotation
- AES-256-GCM encryption via Web Crypto API
- Cookie prefix validation (__Host-, __Secure-)
- Server-level cookie defaults with per-cookie overrides
- Secure auto-detect (auto/true/false)
- Auto prefix validation in serialize
- SameSite=None+Secure enforcement
- 400-day max lifetime validation
- Cookie name RFC token validation
- 4096-byte size limit enforcement
- Zero node:crypto dependency (Bun.CryptoHasher + Web Crypto)
- Monorepo resolve/validate pattern with @zipbul/result

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CookieJar: request-scoped container with get/set/delete/getSetCookieHeaders
  - get() returns ResultAsync (Err for tampered cookies, not throw)
  - set() auto sign+encrypt on getSetCookieHeaders
  - delete() queues maxAge:0 + expires:epoch
  - has() and getRaw() for existence check and third-party cookies
- Remove parse/parseOne (Bun native wrappers with zero added value)
- CookieParser: add isSigningConfigured/isEncryptionConfigured getters
- Fix toErr string literal → CookieErrorReason enum
- Integration/E2E tests rewritten with CookieJar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…n defense

- Partitioned cookies must have Secure attribute (CHIPS spec)
- Domain/path header injection prevention (semicolons, newlines)
  - Bun Cookie constructor validates at construction time
  - CookieParser.serialize() validates as defense-in-depth
- Tests verify both Bun-level and parser-level protection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…validate Max-Age integer

Closes 9 objective defects identified by RFC/NIST + functional verification:

- C1: HMAC now signs (name + 0x00 + value) — prevents cross-cookie replay
  where a signature for cookie A could authenticate as cookie B.
- C2: AES-GCM passes cookie name as AAD (NIST SP 800-38D §5.2.1) — same
  cross-name replay vector closed for encrypted cookies.
- H1+N8: secrets and encryptionSecret enforced >= 32 chars (NIST SP 800-132).
  New WeakSecret reason rejects low-entropy inputs at create time.
- H2: encryptionSecret accepts string | string[] for key rotation; decrypt
  iterates all keys, encrypt uses the first (current) key.
- H3: HMAC verify loop is now constant-time — all secrets are verified, no
  early-exit. Position-based timing oracle closed (4.76x -> 1.03x measured).
- M1: CookieJar.toErr accepts step name; non-CookieError fallback reports
  SignatureVerificationFailed for unsign failures vs DecryptionFailed.
- N6+N7: Max-Age must be a finite integer (RFC 6265 §5.2.2). NaN, Infinity,
  decimals (0.5) now throw InvalidMaxAge in createCookie and serialize.

Tests: 177 pass, 0 fail, 100% line + function coverage.
Build: 10.76 KB ESM. Verified against real Chromium roundtrip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Defects fixed (15):
- secure:'auto' no longer overrides explicit secure values (caller intent preserved via WeakMap meta)
- serialize() applies httpOnly/path/sameSite/partitioned defaults via createCookie
- Expires capped at 400 days (RFC 6265bis §5.5), not just Max-Age
- __Host- prefix rejects empty-string Domain
- createCookie strips null attribute values
- Domain validated against RFC 1034/1123 LDH
- name+value 4096-octet cap (RFC 6265bis §5.6) replaces total-header cap
- per-attribute 1024-octet cap added (RFC 6265bis §5.6)
- prefix detection case-insensitive (RFC 6265bis §5.7)
- single-label domains rejected; publicSuffixCheck hook for full PSL
- createCookie eager validation (Max-Age/Expires/Domain/Path/size)
- secret entropy verified (>= 8 distinct chars); message matches enforcement
- prefixValidation default flipped to true
- wrapBunError fallback uses dedicated CookieParserError reason
- CookieJar.delete() forces sameSite/secure overrides for unblocked deletion

Cryptographic hardening (8):
- HKDF-SHA256/384/512 key derivation (RFC 5869, NIST SP 800-108)
- 4-byte KID prefix in HMAC sigs and AES-GCM ciphertexts; strict KID match
- onEncrypt({keyIndex, counter}) IV-usage hook (NIST SP 800-38D §8.3)
- __Http- / __Host-Http- prefix validation (HttpOnly required)
- Priority=Low|Medium|High attribute support
- jar getSetCookieHeaders() parallelized via Promise.all
- per-instance HKDF-derived key cache
- test/{conformance,security,fuzz}/ tracked in source control

CookieErrorReason values normalized to kebab-case.

Breaking:
- HMAC sig + AES-GCM ciphertext wire formats changed (KID prefix)
- prefixValidation default false -> true
- entropy check rejects low-entropy secrets
- RFC 1123 Domain syntax enforced
- Path must start with "/"

Tests: 330 pass / 510 expects / 99.49% line coverage / 99.62% function coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@parkrevil parkrevil merged commit 390dff3 into main May 11, 2026
1 check passed
@parkrevil parkrevil deleted the feat/cookie branch May 11, 2026 03:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant