Skip to content

e2e tests that spawn a real os serve under vitest inherit TEST=true, which makes better-auth silently skip origin/CSRF validation regardless of NODE_ENV #11267

Description

@os-elon

The gap

Found while writing the regression pin for #11113. Any packages/cli e2e test that
spawns a real os serve child process and constructs its env with
{ ...process.env, ... } — including the shared helper
packages/cli/test/helpers/serve-process.ts's runServe() — carries the vitest
worker's own environment
into the child. Vitest sets TEST=true on its worker
process unconditionally, independent of NODE_ENV.

better-auth 1.7.1 reads TEST directly:

// @better-auth/core/env (env-impl.mjs)constisTest=()=>nodeENV==='test'||toBoolean(env.TEST);// better-auth/dist/context/create-context.mjsskipOriginCheck: options.advanced?.disableOriginCheck!==void0
? options.advanced.disableOriginCheck
: isTest() ? true : false,

So a spawned os serve child that inherits TEST=true has better-auth's own
origin/CSRF validation disabled entirely — independent of what NODE_ENV the
test explicitly sets on the child, and independent of anything plugin-auth's own
trustedOrigins computation does. A test asserting a "closed" security posture
(origin rejected, CSRF enforced) against such a child cannot fail no matter what the
code under test does: the assertion is vacuously satisfied by better-auth's own
test-mode skip, one layer below anything serve.ts or plugin-auth control.

Measured

packages/cli/test/serve-node-env-production-default.e2e.test.ts (#11113) hit this
directly: a boot-real-server-and-probe-the-origin-check pin passed identically with
the fix applied and with the fix reverted, until the child's env explicitly unset
TEST (the same way it already had to unset NODE_ENV). Full repro in that PR's
description and the pin file's own header comment.

Why this is worth its own card

  • It is not local to the one test above — runServe() in
    packages/cli/test/helpers/serve-process.ts spreads ...process.env the same
    way, so every e2e file that imports it inherits the same leak. None of today's
    users of that helper appear to assert origin/CSRF-sensitive behavior (checked
    the ones grepped while writing this card), so the gap is currently latent rather
    than an active false-green — but it is silent and structural, not something a
    future author would have any reason to suspect.
  • The failure mode is the dangerous direction: a security-posture assertion that
    can never go red for the reason it's supposed to test is worse than no
    assertion, because it reads as coverage.

Not claimed

I did not audit every e2e test that spawns a real CLI process for whether it
happens to touch auth/CSRF-sensitive behavior today, and I did not assess whether
VITEST=true (also inherited) has a similar effect anywhere else in the
dependency tree — that survey is the bulk of the work and belongs to whoever
takes this.

Options, not recommended here

  1. Strip TEST (and maybe VITEST, VITEST_WORKER_ID, VITEST_POOL_ID,
    VITEST_MODE) in runServe()'s own env construction, once, so every current
    and future caller is covered without having to know the trap exists.
  2. Document the trap at runServe()'s call site and leave each e2e author to
    handle it themselves (what Dev-only gates spelled NODE_ENV !== 'production' still open in a real production deployment that never sets NODE_ENV (os serve does not force it, os start does) #11113's pin does, since it doesn't use
    runServe()).
  3. Leave it — the gap is latent today per the measurement above.

Related: #11113 (where this was found and worked around locally, not fixed
repo-wide).

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions