feat(worker): proxy security hardening — 7-layer defense-in-depth stack#62
Merged
wgordon17 merged 56 commits intoApr 15, 2026
Merged
Conversation
- sealApiToken: add purpose parameter, include in POST body (CRIT-001, 6/7 reviewers) - ensureSession: wrap issueSession in try/catch, fallback to random sessionId on error (SEC-002, STRUCT-005) - handleProxySeal: add VALID_PURPOSES allowlist + 64-char max-length for purpose field (SEC-003, QA-002) - validateAndGuardProxyRoute: include CORS headers on validation error responses (SEC-004) - session.ts: cache derived HMAC keys at module level to avoid repeated HKDF derivation (PERF-001/002) - turnstile.ts: add 5s AbortController timeout to siteverify fetch (PERF-003) - proxy.test.ts: update sealApiToken calls with purpose, assert body.purpose field, add error field test - seal.test.ts: update purpose values to match VALID_PURPOSES allowlist - crypto.test.ts: add cross-purpose isolation test (F-003)
…path, adds rate limiter error test
- adds Turnstile token length guard (>2048) with boundary tests - adds seal key derivation cache (_sealKeyCache Map, bounded by VALID_PURPOSES) - passes pre-parsed pathname to validateAndGuardProxyRoute (eliminates redundant URL parse) - unifies session key cache into single Map (removes duplicate getSessionHmacPrevKey) - fixes structured error logging in ensureSession catch path - removes dead options parameter from validateProxyRequest - corrects HKDF key material descriptions in CryptoEnv and DEPLOY.md - fixes test key comments to match actual decoded values - adds 14 new tests covering previously-untested paths
- Sets retry: 'never' on widget options to prevent setTimeout leak when turnstile.remove() is called during a retry cycle - Wraps render+execute in turnstile.ready() per Cloudflare docs to guard against race conditions with script loading - Hoists widgetId to outer scope so timeout cleanup works across the ready() callback boundary - Adds ready() and retry to Turnstile type declarations - Adds ready mock to test fixture
- Adds settled guard at start of ready() callback to prevent widget creation after timeout fires - Wraps render+execute in try/catch for immediate rejection on render failure instead of 30s hang - Wires timeout-callback to reject immediately on Turnstile internal challenge timeout - Makes X-Requested-With header non-overridable in proxyFetch - Adds tests: timeout-callback, X-Requested-With non-override, retry:never assertion, ready() call assertion
- Clears 30s setTimeout in cleanup() to prevent timer leak on normal promise resolution - Moves execute() inside try/catch alongside render() so both throw paths reject immediately - Wraps turnstile.remove() in try/catch inside cleanup() to prevent remove() failures from blocking reject() - Adds test for render() throwing synchronously
- Ensures timeoutId is assigned before ready() fires so cleanup() can clear the timer even when the callback resolves synchronously
- adds HKDF-SHA256 KAT (RFC 5869 A.1) and AES-256-GCM KAT (McGrew-Viega TC14) - adds 6 property-based fuzz tests via @fast-check/vitest - switches verifySession to SHA-256 pre-hash + timingSafeEqual (CF Workers) - adds SubtleCrypto.timingSafeEqual type augmentation - fixes test key construction to produce actual 32-byte keys
- Extracts createIpRateLimiter factory from token exchange rate limiter - Adds IP rate limiting to Sentry tunnel (15/min), CSP report (15/min) - Adds strict origin check to Sentry tunnel via validateOrigin - Adds soft origin check to CSP report (allows absent Origin) - Adds Content-Length pre-check before body reads on both tunnels - Adds IP pre-gate (60/min) before ensureSession on proxy routes - Removes dead GitLab route, purpose, and test references - Documents tunnel security model in DEPLOY.md - Migrates Sentry tests to dedicated sentry-tunnel.test.ts - Extracts shared test helpers to tests/worker/helpers.ts
- Adds getClientIp helper with missing-header warning for misconfiguration - Removes raw SEAL_KEY from cache key (purpose + fingerprint rotation) - Adds CSP field sanitization: control chars stripped, 2048-char cap - Adds durable rate limiting to token exchange via PROXY_RATE_LIMITER - Adds consecutive failure tracking for durable rate limiter - Standardizes consoleSpy naming across test files
- getClientIp returns null instead of falling back to 'unknown' - All endpoints reject 400 when CF-Connecting-IP is absent - PROXY_RATE_LIMITER binding validated before use (503 if missing) - Transient .limit() errors still fail open; missing binding fails closed - Tests updated to include CF-Connecting-IP on all inline requests
- Tests: missing CF-Connecting-IP returns 400 (sentry, csp, oauth, seal) - Tests: missing PROXY_RATE_LIMITER binding returns 503 (oauth, seal) - DEPLOY.md: documents CF-Connecting-IP requirement, binding validation, CSP field sanitization, durable token exchange rate limiting, Content-Length byte ceilings
- Remove hardcoded SENTRY_DSN constant; read from import.meta.env.VITE_SENTRY_DSN at runtime - Replace hardcoded allowUrls regex with window.location.origin (string prefix match) - Add VITE_SENTRY_DSN and VITE_TURNSTILE_SITE_KEY to deploy.yml build env - Document VITE_SENTRY_DSN in .env.example - Add initSentry tests covering no-op (empty DSN), DSN forwarding, and allowUrls
- Removes 'https://gh.gordoncode.dev' from ALLOWED_ORIGINS_DEFAULT - Adds startup warning when MCP_RELAY_ALLOWED_ORIGINS is not set - Documents MCP_RELAY_ALLOWED_ORIGINS in mcp/README.md
- Accepts optional base_url argument in waf-smoke-test.sh - Gates WAF smoke test in deploy.yml on DEPLOY_DOMAIN variable - Adds DEPLOY comments to wrangler.toml for routes and SENTRY_DSN vars - Moves SENTRY_DSN from wrangler.toml [vars] to wrangler secret (commented template)
- Creates scripts/validate-deploy.sh with CI and local modes - CI mode checks required env vars (fails on VITE_GITHUB_CLIENT_ID, CF credentials; warns on optional) - Local mode checks CF Worker secrets via wrangler secret list - Adds validate:deploy script to package.json - Adds validation step to deploy.yml before build (never echoes secret values)
- Adds Fork Deployment Checklist section before GitHub Actions section - Covers required steps, optional config, and static-only deployment guide - Updates OAuth App URLs to use YOUR-DOMAIN placeholder - Updates ALLOWED_ORIGIN example to use YOUR-DOMAIN placeholder - Updates WAF rule expression to use YOUR-DOMAIN placeholder - Updates SENTRY_DSN docs to reflect Worker secret (not wrangler.toml [vars])
- fix SEAL_KEY fingerprint collision (full key comparison, not 8-char slice) - redesign _sessionKeyCache with fingerprint+purpose pattern - add Turnstile action binding validation (action:'seal') - fix Sentry allowUrls: anchored RegExp with boundary - fix DEPLOY.md: CSP origin check documented as strict - add TURNSTILE_SECRET_KEY to validate-deploy.sh - fix CSP rate-limit test to send valid requests with Origin - add tests: 30s timeout, script reuse, jira-refresh-token, SEAL_KEY rotation, Turnstile action mismatch integration - fix toBase64Url: spread instead of byte-by-byte concat - fix stale comment, remove redundant X-Requested-With
- replace inline btoa/atob+replace chains with toBase64Url/fromBase64Url - remove spurious blank line in sentry-tunnel.test.ts
e7d6aad to
4953c38
Compare
- Move security analysis (rate limits, WAF exemptions, threat model) from DEPLOY.md to hack/docs/security-runbook.md - Add SENTRY_SECURITY_TOKEN to .dev.vars.example, DEPLOY.md, validate-deploy.sh - Simplify verifySession: 3 crypto ops to 1 (crypto.subtle.verify) - Consolidate getCorsHeaders/getProxyCorsHeaders into buildCorsHeaders - Fix CSP scrub ordering: scrub URLs before 2048-char truncation - Remove design-doc codes (SDR-/SC-/ADV-) from all source and test files - Remove purpose.length > 64 dead code from seal endpoint - Fix wrangler.toml rate limit: period=10 limit=10 (CF free-tier) - Add tests: CSP token prefix scrubbing, SENTRY_SECURITY_TOKEN forwarding, non-string token validation, Set-Cookie on 405, crypto.subtle.verify spy - Export shared ALLOWED_ORIGIN from test helpers
Adds https://gh.gordoncode.dev to ALLOWED_ORIGINS_DEFAULT so the WebSocket relay works out of the box for deployed SPA users. MCP_RELAY_ALLOWED_ORIGINS env var remains for custom domains/forks.
Adds a test that ensures validate-deploy.sh stays in sync with the TypeScript Env interfaces. Catches missing, stale, and unknown Worker secrets. Also warns about key rotation secrets (SEAL_KEY_PREV, SESSION_KEY_PREV) that were previously unchecked.
Move static-only deployment section above OAuth setup to present both paths as equal choices. Replace 'Required' heading with 'OAuth + Cloudflare Worker' to clarify the backend is only needed for OAuth. Tag Worker-specific optional items so static deployers can skip them.
validate-deploy.sh: - Remove --ci flag; single path checks VITE_ vars + wrangler secrets - Resolve wrangler via node_modules/.bin (not just global PATH) - Use --format json (wrangler v3 dropped --json flag) - Fix grep patterns for pretty-printed JSON (whitespace-tolerant) - Check .env files as fallback for VITE_ vars (Vite loads at build) Key rotation (_PREV → _NEXT): - Rename SESSION_KEY_PREV/SEAL_KEY_PREV to _NEXT variants - Sign/seal with NEXT key during rotation, verify/unseal with both - Fixes operational flow: _PREV required knowing the current secret value, which is impossible to retrieve from Cloudflare (write-only) - _NEXT only requires the value you just generated
Session keys rotate in 8 hours (cookie Max-Age). Seal keys protect localStorage tokens with no expiry — promoting SEAL_KEY before all clients re-seal makes old tokens permanently unreadable.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary