Skip to content

fix(auth): require a live session for bearer tokens - #2041

Open
liutingqiu wants to merge 1 commit into
Commitlabs-Org:masterfrom
liutingqiu:fix/1926-require-live-session
Open

liutingqiu wants to merge 1 commit into
Commitlabs-Org:masterfrom
liutingqiu:fix/1926-require-live-session

Conversation

@liutingqiu

Copy link
Copy Markdown

Context

Follows #1926. requireWalletAuth in src/lib/backend/preferences.ts is the bearer guard used by the user-preferences routes and the notifications routes. After the real session lookup it had a format-based fallback:

const session = verifySessionToken(token);
if (session.valid && session.address) return session.address;

// Fallback for session token placeholder format in tests: session_<address>_<timestamp>
const match = token.match(/^session_([A-Za-z0-9]+)_\d+$/);
if (match && match[1]) return match[1];   // ← trusts an address decoded from the token

That branch returns an address without any session existing. Real tokens are session_<16 random bytes as hex> — createSessionToken builds them as `session_${randomBytes(16).toString('hex')}`, which contains one underscore. The regex requires two, so it cannot match a genuine token: the only input that ever reaches return match[1] is a forged string of the form session_<anything>_<digits>. Anyone could therefore send Bearer session_<victim address>_1 and be authenticated as that wallet, on both the preferences and notifications endpoints.

What changed

  • src/lib/backend/preferences.ts — removed the fallback; the address now comes only from a record returned by verifySessionToken, so a token that does not match a live session is a 401. The stale doc comment (which described the placeholder format as the supported one) and the TODO: Replace with proper JWT verification are updated to describe what the function now actually does. The malformed-header checks are unchanged.
  • src/app/api/user/preferences/route.test.ts — the suite authenticated with hard-coded session_<address>_<timestamp> strings, i.e. it was passing because of the vulnerability. Tokens are now minted per test with createSessionToken after _clearStores() in beforeEach. All existing assertions kept their meaning (per-wallet isolation, 401 cases, ETag, idempotency, concurrency).
  • src/app/api/notifications/__tests__/route.test.ts — same fake-token pattern, same fix. That route shares this guard, so the change is not scoped to preferences alone.
  • New tests in the preferences suite: a live session resolves to its address; a forged session_<address>_<digits> token is rejected; and a bare address is rejected.

I proved the new test catches the bug rather than assuming it: with the old fallback temporarily restored, requireWalletAuth rejects a forged token that encodes a wallet address fails (1 failed / 29 passed); with the fix it passes (30/30).

How to verify

npm ci
npx vitest run src/app/api/user/preferences/route.test.ts src/app/api/notifications/__tests__/route.test.ts
npm run format:check
npx tsc --noEmit

Local results on this branch (Windows, node v24.21.0 — not the pinned Node 20 from .nvmrc, so CI on Node 20 is the authoritative run):

  • The two affected suites: 61/61 pass (30 + 31).
  • npx prettier --check and npx eslint on all changed files: clean.
  • npx tsc --noEmit: 58 errors, identical to unmodified master in this environment, none in the changed files. The job is continue-on-error: true while the pre-existing backlog is burned down (see docs/CI.md).
  • Full npm run test:coverage: the failing-test set is byte-for-byte identical to master's (192 failing items, no additions, no removals), and neither changed test file fails. The suite also gains exactly the 1 net new test (3 added, 2 replaced).

Behaviour note for reviewers

This is a breaking change for any client that was relying on the placeholder format. It is not a real client path: that format was only ever produced by tests, and verifySessionToken is the sole legitimate way a token becomes valid. The Bearer contract and all success paths via createSessionToken are unchanged.

Closes #1926

requireWalletAuth fell back to decoding an address out of a session_<address>_<digits> token when no session existed. Tokens created by createSessionToken contain a single underscore, so that pattern can only ever match forged input, and it let any caller authenticate as any wallet on the preferences and notifications routes.

Drop the fallback so the address is only taken from a live server-side session, and mint real tokens in the two test suites that were authenticating through the removed branch.
@vercel

vercel Bot commented Oct 1, 2026

Copy link
Copy Markdown

@liutingqiu is attempting to deploy a commit to the 1nonly's projects Team on Vercel.

A member of the Team first needs to authorize it.

This branch has not been deployed

No deployments
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.

requireWalletAuth in src/lib/backend/preferences.ts accepts forgeable session tokens

1 participant