Skip to content

test(auth): prove disabledPaths is wired, not just declared - #26

Merged
chriskehayias merged 1 commit into
devfrom
test/disabled-paths-router-enforcement
Sep 13, 2026
Merged

chriskehayias merged 1 commit into
devfrom
test/disabled-paths-router-enforcement

Conversation

@chriskehayias

Copy link
Copy Markdown
Contributor

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 — disabledPaths carries all six paths and is wired at auth.ts:347. The tests were the problem: they asserted only that disabledAuthPaths, an exported array constant, contains those paths. Nothing tied that array to the running router.

Verified by doing it, not inferred: commenting out disabledPaths: disabledAuthPaths deleted the defense-in-depth control outright, and the entire 805-test suite stayed green.

Why route.test.ts didn't already cover this

It reads like it does, which is the trap. It mocks @/lib/auth to {} and mocks toNextJsHandler, 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.handler with real Request objects, deliberately bypassing Next.js routing and the allowlist. The allowlist is the primary control; disabledPaths is the layer behind it. The advisory is explicit that the allowlist is an addition, not a replacement, so both must hold independently.

Test Asserts
POST /update-user with the advisory's exact payload (a well-formed foreign User_GUID) 404
Every other disabledAuthPaths entry, table-driven 404
CONTROL: /get-session not 404

The 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-user is refused on the path, before body parsing. userGuid must stay input: true for sign-in to work, so no field validator could distinguish this request from mapProfileToUser.

Verification

Unwiring disabledPaths now fails 4 tests:

× 404s POST /update-user, the session-identity takeover vector
× 404s the other identity-mutating endpoint /change-email
× 404s the other identity-mutating endpoint /change-password
× 404s the other identity-mutating endpoint /delete-user

812 tests pass, eslint . clean.

Reviewer note

/set-password and /delete-user/callback pass 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:

  • Rotate 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.
  • Review dp_Audit_Log for 2026-07-09 → 2026-09-13 — this repo's own 65-day exposure window (e599075e94e549). Forged writes carry the impersonated user's User_ID.

🤖 Generated with Claude Code

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

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@chriskehayias
chriskehayias merged commit 84ef832 into dev Sep 13, 2026
2 checks passed
@chriskehayias
chriskehayias deleted the test/disabled-paths-router-enforcement branch September 13, 2026 11:33
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