Skip to content

fix(auth): gate the dev secret on the dev flag, not the request host - #233

Closed
shulaoda wants to merge 2 commits into
mainfrom
06-26-fix_auth_gate_the_dev_secret_on_the_dev_flag_not_the_request_host
Closed

shulaoda wants to merge 2 commits into
mainfrom
06-26-fix_auth_gate_the_dev_secret_on_the_dev_flag_not_the_request_host

Conversation

@shulaoda

Copy link
Copy Markdown
Member

Summary (the vulnerability)

resolveSecret in packages/void/src/runtime/better-auth-shared.ts returned a hardcoded fallback secret (void-dev-better-auth-secret-1234567890) whenever BETTER_AUTH_SECRET was unset and the request's hostname looked local (localhost, 127.0.0.1, or *.local). It derived that hostname from new URL(request.url).hostname.

Why this is a real, exploitable issue (necessity)

On a deployed Cloudflare Worker, request.url is reconstructed from the client-supplied Host / :authority header, so the hostname check is fully attacker-controlled. Against a Worker that was deployed without BETTER_AUTH_SECRET, an attacker only has to send Host: localhost (or Host: anything.local) for resolveSecret to hand Better Auth the well-known constant secret. Better Auth then signs and verifies session cookies with a secret the attacker also knows, which permits session/cookie forgery and full account impersonation. Because resolveSecret is reached on every auth request via resolveVoidAuthOptions (used by the auth middleware, the auth handler, and the WebSocket auth path), this is not an edge case but the whole authentication surface. The hardcoded secret is convenient for local development, so it cannot simply be deleted; it has to be made unreachable in production.

Fix

The dev fallback is now gated on a build-time signal that the request cannot influence:

// better-auth-shared.ts
function resolveSecret(env: VoidAuthEnv, dev: boolean): string {
  const explicit = typeof env.BETTER_AUTH_SECRET === 'string' && env.BETTER_AUTH_SECRET.length > 0
    ? env.BETTER_AUTH_SECRET
    : undefined;
  if (explicit) return explicit;
  if (dev) return 'void-dev-better-auth-secret-1234567890';
  throw new Error('auth: Missing Better Auth secret. Set BETTER_AUTH_SECRET.');
}

// resolveVoidAuthOptions
secret: resolveSecret(env, runtime.dev === true),

VoidAuthRuntimeOptions gains an optional dev?: boolean, and the generated worker entry bakes the value in as a literal: compile.ts now emits dev: ${options.dev === true} into __voidAuthOptions. options.dev is config.command === 'serve', so it is true only under vite dev and false for every production build. isLocalHost and the new URL(request.url) lookup are removed.

Why this is correct (correctness)

The dev signal is authoritative and request-independent: options.dev is the same config.command === 'serve' flag already used elsewhere in codegen (for example __VOID_DEV_ROUTING), and it is serialized into the generated module as a constant, so nothing an HTTP client sends can change it. Baking a literal (rather than emitting import.meta.env.DEV) avoids depending on Vite replacing that expression inside the published void package, where bundling of dependency code is not guaranteed to substitute it. The change is fail-closed: dev is optional and defaults to falsy, so every production build and any direct caller that does not opt in gets the throw path, and a configured BETTER_AUTH_SECRET continues to take precedence over everything. Local development is unaffected because vite dev sets options.dev === true, so the fallback still applies there.

Behavior change

A deployed Worker that has no BETTER_AUTH_SECRET now fails closed with auth: Missing Better Auth secret. Set BETTER_AUTH_SECRET. instead of silently running on the shared dev secret. That previous state was insecure by definition, so failing loudly is the intended outcome; production deployments must set BETTER_AUTH_SECRET (which void deploy already enforces for prod secrets).

Testing

Updated the two affected unit tests and added a regression test asserting that a spoofed Host: localhost with no dev flag throws rather than returning the fallback. Behavior was also verified by driving resolveVoidAuthOptions directly across three cases: dev: true on a non-localhost host returns the fallback, a spoofed Host: localhost with dev unset throws, and an explicit BETTER_AUTH_SECRET wins. tsgo typecheck passes.

@shulaoda
shulaoda marked this pull request as draft June 25, 2026 22:54
@shulaoda
shulaoda marked this pull request as ready for review June 30, 2026 23:42
@shulaoda
shulaoda marked this pull request as draft June 30, 2026 23:46
cpojer pushed a commit that referenced this pull request Sep 11, 2026
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
@cpojer cpojer closed this in 6fdb39f Sep 30, 2026
@cpojer

cpojer commented Sep 30, 2026

Copy link
Copy Markdown
Member

Done on main.

@shulaoda
shulaoda deleted the 06-26-fix_auth_gate_the_dev_secret_on_the_dev_flag_not_the_request_host branch September 30, 2026 02:41
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.

2 participants