Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(security): run the JWT_SECRET production check on every request - #1097

Draft
mmcintosh wants to merge 1 commit into
mainfrom
fix/jwt-secret-check-every-request
Draft

fix(security): run the JWT_SECRET production check on every request#1097
mmcintosh wants to merge 1 commit into
mainfrom
fix/jwt-secret-check-every-request

Conversation

@mmcintosh

Copy link
Copy Markdown
Collaborator

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, after bootstrapComplete = 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 of JWT_SECRET / CORS_ORIGINS / ENVIRONMENT values, warnings logged once, the same Error rethrown 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_SECRET now also returns 500 on:

  • /health and static assets — previously returned early and never reached the check
  • Better Auth's session endpoints (/auth/sign-in/..., the login path) — same
  • and metricsMiddleware runs first, so those 500s are recorded as requests

That is the "fail loud" the check was written for, applied everywhere instead of everywhere-but-where-it-was-inconvenient. An uptime probe on /health will see it. That is the point.

What this does NOT do — #1043 stays open

  • The hard-fail is gated on ENVIRONMENT === "production". The starter wrangler.toml sets "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 missing ENVIRONMENT to fail closed, or the fallback removed.
  • The fallback constant lives in three files, not one: middleware/auth.ts:15, middleware/csrf.ts:22 (keys the CSRF HMAC), routes/auth.ts:22. Plus the legacy password salt at middleware/auth.ts:306. None touched here.

Tests

Real Hono app 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 --noEmit clean. 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

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
@mmcintosh
mmcintosh requested a review from lane711 as a code owner September 13, 2026 05:18
@mmcintosh
mmcintosh marked this pull request as draft September 13, 2026 16:24
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