fix: prevent double encoded cookie (#8133)

Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
Oluwatobi Mustapha
2026-03-03 05:23:06 +01:00
committed by GitHub
parent 47bba48f28
commit 49921100ae
7 changed files with 52 additions and 5 deletions

View File

@@ -1,3 +1,11 @@
function tryDecode(str: string): string {
try {
return decodeURIComponent(str);
} catch {
return str;
}
}
export interface CookieAttributes {
value: string;
"max-age"?: number | undefined;
@@ -85,7 +93,8 @@ export function parseSetCookieHeader(
return;
}
const attrObj: CookieAttributes = { value };
const decodedValue = value.includes("%") ? tryDecode(value) : value;
const attrObj: CookieAttributes = { value: decodedValue };
attributes.forEach((attribute) => {
const [attrName, ...attrValueParts] = attribute!.split("=");

View File

@@ -226,6 +226,12 @@ describe("cookie-utils parseSetCookieHeader", () => {
);
});
it("decodes URI-encoded cookie values", () => {
const header = "token=hello%20world%3Dfoo; Path=/";
const map = parseSetCookieHeader(header);
expect(map.get("token")?.value).toBe("hello world=foo");
});
it("handles cookie with Expires followed by cookie without Expires", () => {
const map = parseSetCookieHeader(
"session=xyz; Expires=Mon, 01 Jan 2026 00:00:00 GMT, token=abc",

View File

@@ -100,7 +100,7 @@ export const nextCookies = () => {
path: value.path,
} as const;
try {
cookieHelper.set(key, decodeURIComponent(value.value), opts);
cookieHelper.set(key, value.value, opts);
} catch {
// this will fail if the cookie is being set on server component
}

View File

@@ -78,7 +78,7 @@ export const sveltekitCookies = (
for (const [name, { value, ...ops }] of parsed) {
try {
event.cookies.set(name, decodeURIComponent(value), {
event.cookies.set(name, value, {
sameSite: ops.samesite,
path: ops.path || "/",
expires: ops.expires,

View File

@@ -51,7 +51,7 @@ export const tanstackStartCookies = () => {
path: value.path,
} as const;
try {
setCookie(key, decodeURIComponent(value.value), opts);
setCookie(key, value.value, opts);
} catch {
// this will fail if the cookie is being set on server component
}

View File

@@ -51,7 +51,7 @@ export const tanstackStartCookies = () => {
path: value.path,
} as const;
try {
setCookie(key, decodeURIComponent(value.value), opts);
setCookie(key, value.value, opts);
} catch {
// this will fail if the cookie is being set on server component
}

View File

@@ -1,5 +1,6 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { createAuthClient } from "../../client";
import { parseSetCookieHeader } from "../../cookies";
import { getTestInstance } from "../../test-utils/test-instance";
import type { BetterAuthOptions } from "../../types";
import { admin } from "../admin";
@@ -81,6 +82,37 @@ describe("Custom Session Plugin Tests", async () => {
});
});
it("should not double-encode session cookie during get-session refresh", async () => {
const { headers } = await signInWithTestUser();
const signedInCookie = headers.get("cookie");
const signedInSessionToken = signedInCookie?.match(
/better-auth\.session_token=([^;]+)/,
)?.[1];
expect(signedInSessionToken).toBeDefined();
let refreshedSessionToken: string | undefined;
await client.getSession({
fetchOptions: {
headers,
onResponse(context) {
const setCookies = context.response.headers.getSetCookie();
for (const cookieStr of setCookies) {
const parsed = parseSetCookieHeader(cookieStr);
const token = parsed.get("better-auth.session_token")?.value;
if (token) {
refreshedSessionToken = token;
break;
}
}
},
},
});
expect(refreshedSessionToken).toBeDefined();
expect(refreshedSessionToken).toBe(signedInSessionToken);
expect(refreshedSessionToken).not.toContain("%25");
});
it("should return the custom session for multi-session", async () => {
const headers = new Headers();
const testUser = {