fix(security): run the JWT_SECRET production check on every request - #1097
Draft
mmcintosh wants to merge 1 commit into
Draft
fix(security): run the JWT_SECRET production check on every request#1097mmcintosh wants to merge 1 commit into
mmcintosh wants to merge 1 commit into
Conversation
verifySecurityConfig() is what hard-fails a production deploy that is running on the hardcoded JWT fallback secret. It was called once, at the bottom of the cold-start bootstrap path, after `bootstrapComplete = true` had already been set — so a warm isolate never ran it, and the KV fast-path (the normal path once any isolate has bootstrapped once) returned before reaching it. In a real deployment the hard-fail essentially never fired. Refs #1043. The call moves to the top of bootstrapMiddleware, ahead of both short-circuits, so it cannot be skipped. Memoised per isolate rather than re-run raw on every request. The three bindings it reads are fixed for the life of an isolate, so its verdict is too; re-running it only repeated the same console.warn lines on every request (measured: two per request in a bare dev env, one per asset fetch under `wrangler dev`). The verdict is computed once per distinct set of values, the warnings are logged once, and the same Error is rethrown on every later request so a misconfigured production deploy keeps failing closed. Keyed on the values, not the env object, because the Node adapter builds a fresh env per request. resetBootstrap() takes an optional value so tests can put an isolate into the warm state without mocking the cold-start DB flow, and it also forgets the memoised verdict. No existing callers. Behaviour changes, all deliberate: a misconfigured production deploy now also 500s on /health, on static assets, and on Better Auth's session endpoints (/auth/sign-in/... is the login path), none of which reached the old call site; and metricsMiddleware runs first, so those 500s are recorded. Does not close #1043. The hard-fail is gated on ENVIRONMENT=production, which the starter wrangler.toml only sets under [env.production] and the self-host entry defaults to "development", so a deployer who configured nothing still gets warnings rather than a refusal. The fallback constant also lives in three files (middleware/auth.ts, middleware/csrf.ts, routes/auth.ts), untouched here. Tests: a real Hono app through the real middleware, one per short-circuit (warm isolate, fresh isolate, KV fast-path with a spy proving KV was never read), plus the memo's three properties: it keeps throwing, it warns once, and a changed env is re-verified. Against the old placement five of the seven fail with 200; against a non-memoised version of this fix the warn-once test fails with 3 calls. Claude-Session: https://claude.ai/code/session_01BDQXWAZcx95WBwrHEbCtrf
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.
What
verifySecurityConfig()— the check that hard-fails a production deploy running on the hardcoded JWT fallback — was called once, at the bottom of the cold-start bootstrap path, afterbootstrapComplete = true. A warm isolate never ran it and the KV fast-path returned before reaching it. In a real deployment it essentially never fired. Refs #1043.The call moves to the top of
bootstrapMiddleware, ahead of both short-circuits, and is memoised per isolate: verdict computed once per distinct set ofJWT_SECRET/CORS_ORIGINS/ENVIRONMENTvalues, warnings logged once, the sameErrorrethrown on every later request. Without the memo the warnings repeat on every request (measured: two per request in a bare dev env).resetBootstrap()takes an optional value so tests can put an isolate in the warm state; it also forgets the memo. No existing callers.Behaviour changes — deliberate, please read
A production deploy with a missing or default
JWT_SECRETnow also returns 500 on:/healthand static assets — previously returned early and never reached the check/auth/sign-in/..., the login path) — samemetricsMiddlewareruns first, so those 500s are recorded as requestsThat is the "fail loud" the check was written for, applied everywhere instead of everywhere-but-where-it-was-inconvenient. An uptime probe on
/healthwill see it. That is the point.What this does NOT do — #1043 stays open
ENVIRONMENT === "production". The starterwrangler.tomlsets"development"at top level and"production"only under[env.production]; the self-host entry defaults to"development". A deployer who configured nothing gets warnings, not a refusal. Closing [Vulnerability] Hardcoded JWT Secret May Lead to Privilege Escalation Risks #1043 needs either a missingENVIRONMENTto fail closed, or the fallback removed.middleware/auth.ts:15,middleware/csrf.ts:22(keys the CSRF HMAC),routes/auth.ts:22. Plus the legacy password salt atmiddleware/auth.ts:306. None touched here.Tests
Real
Honoapp through the real middleware, one test per short-circuit: warm isolate, fresh isolate, and the KV fast-path with a spy proving KV was never read (an ordering claim, not a second copy of the warm-isolate test). Plus the memo's three properties: keeps throwing, warns once, re-verifies when the bindings change.Mutation-checked: against the old placement, five of the seven fail with 200; against a non-memoised version of this fix, the warn-once test fails with 3 calls. Full core suite 1937 passed / 247 skipped,
tsc --noEmitclean. No Playwright spec: the behaviour under test is a deploy with production bindings missing, which CI's dev server cannot represent.Conflicts
Touches the same function as #1019 (which keeps the old call site after a new
finally) and #1015 (KV fast-path). Whichever lands second re-reads the function.https://claude.ai/code/session_01BDQXWAZcx95WBwrHEbCtrf