You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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.
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
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.
The gap
Found while writing the regression pin for #11113. Any
packages/clie2e test thatspawns a real
os servechild process and constructs itsenvwith{ ...process.env, ... }— including the shared helperpackages/cli/test/helpers/serve-process.ts'srunServe()— carries the vitestworker's own environment into the child. Vitest sets
TEST=trueon its workerprocess unconditionally, independent of
NODE_ENV.better-auth 1.7.1 reads
TESTdirectly:So a spawned
os servechild that inheritsTEST=truehas better-auth's ownorigin/CSRF validation disabled entirely — independent of what
NODE_ENVthetest explicitly sets on the child, and independent of anything
plugin-auth's owntrustedOriginscomputation 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.tsorplugin-authcontrol.Measured
packages/cli/test/serve-node-env-production-default.e2e.test.ts(#11113) hit thisdirectly: 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
envexplicitly unsetTEST(the same way it already had to unsetNODE_ENV). Full repro in that PR'sdescription and the pin file's own header comment.
Why this is worth its own card
runServe()inpackages/cli/test/helpers/serve-process.tsspreads...process.envthe sameway, 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.
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 thedependency tree — that survey is the bulk of the work and belongs to whoever
takes this.
Options, not recommended here
TEST(and maybeVITEST,VITEST_WORKER_ID,VITEST_POOL_ID,VITEST_MODE) inrunServe()'s own env construction, once, so every currentand future caller is covered without having to know the trap exists.
runServe()'s call site and leave each e2e author tohandle it themselves (what Dev-only gates spelled
NODE_ENV !== 'production'still open in a real production deployment that never sets NODE_ENV (os servedoes not force it,os startdoes) #11113's pin does, since it doesn't userunServe()).Related: #11113 (where this was found and worked around locally, not fixed
repo-wide).