test(auth): prove disabledPaths is wired, not just declared - #26
Merged
Merged
Conversation
The F-UPDATE-USER advisory requires the disabled endpoints to 404 at the
Better Auth router. This repo asserted only that `disabledAuthPaths` — an
exported array constant — contains those paths.
Nothing tied that array to the running router. Commenting out
`disabledPaths: disabledAuthPaths` in the `betterAuth()` options deleted the
defence-in-depth control outright and the entire 805-test suite stayed green.
That was verified by doing it, not inferred.
`src/app/api/auth/[...all]/route.test.ts` did not cover the gap either,
contrary to how it reads: it mocks `@/lib/auth` to `{}` and mocks
`toNextJsHandler`, so it exercises the allowlist WRAPPER against a stub and
never reaches real Better Auth. Correct for what it tests, but it means no
test in the suite drove the real router.
These tests call `auth.handler` with real `Request`s, deliberately bypassing
Next.js routing and the allowlist, because the allowlist is the primary
control and `disabledPaths` is the layer behind it. The advisory is explicit
that the allowlist is an addition, not a replacement, so both must hold
independently.
Coverage:
- `POST /update-user` with the advisory's exact payload (a well-formed
foreign `User_GUID`) → 404. Refused on the path, before body parsing:
`userGuid` is necessarily `input: true`, so no validator could distinguish
this from `mapProfileToUser`.
- Every other entry in `disabledAuthPaths`, table-driven → 404.
- CONTROL: `/get-session` is NOT 404. Without it the block could pass
vacuously — a handler that 404s everything would satisfy all of the above.
Negative control: unwiring `disabledPaths` now fails 4 tests, including
`/update-user`. Note `/set-password` and `/delete-user/callback` pass in both
states — Better Auth does not mount them without an email/password provider,
so those two assert intent rather than active protection, and should not be
counted as coverage.
812 tests pass, eslint clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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.
Summary
Closes a test gap found while verifying the F-UPDATE-USER advisory (session identity reassignable via
/update-user) against this repo.The code fix is present and correct —
disabledPathscarries all six paths and is wired atauth.ts:347. The tests were the problem: they asserted only thatdisabledAuthPaths, an exported array constant, contains those paths. Nothing tied that array to the running router.Verified by doing it, not inferred: commenting out
disabledPaths: disabledAuthPathsdeleted the defense-in-depth control outright, and the entire 805-test suite stayed green.Why
route.test.tsdidn't already cover thisIt reads like it does, which is the trap. It mocks
@/lib/authto{}and mockstoNextJsHandler, so it exercises the allowlist wrapper against a stub. That's correct for what it tests — and its negative control is genuine — but it never reaches real Better Auth. No test in the suite drove the real router.What these tests do
They call
auth.handlerwith realRequestobjects, deliberately bypassing Next.js routing and the allowlist. The allowlist is the primary control;disabledPathsis the layer behind it. The advisory is explicit that the allowlist is an addition, not a replacement, so both must hold independently.POST /update-userwith the advisory's exact payload (a well-formed foreignUser_GUID)disabledAuthPathsentry, table-driven/get-sessionThe control is what stops the block passing vacuously — a handler that 404s everything (bad base path, broken instance) would otherwise satisfy every assertion above it.
/update-useris refused on the path, before body parsing.userGuidmust stayinput: truefor sign-in to work, so no field validator could distinguish this request frommapProfileToUser.Verification
Unwiring
disabledPathsnow fails 4 tests:812 tests pass,
eslint .clean.Reviewer note
/set-passwordand/delete-user/callbackpass in both states — Better Auth doesn't mount them without an email/password provider, so they 404 naturally. Those two assert intent rather than active protection and shouldn't be counted as coverage. Kept because they'd become protective if a future version or config change starts mounting them.Not closed by this PR
Two advisory items no test can satisfy, for the maintainer:
BETTER_AUTH_SECRET— patching doesn't revoke an already-forged session, which survives in the JWT cookie cache for up to an hour with no server-side store to clear.dp_Audit_Logfor 2026-07-09 → 2026-09-13 — this repo's own 65-day exposure window (e599075→e94e549). Forged writes carry the impersonated user'sUser_ID.🤖 Generated with Claude Code