Skip to content

Commit 55dd06e

Browse files
fix: prevent double encoded cookie (#8133)
Co-authored-by: Alex Yang <himself65@outlook.com>
1 parent 5d7dd9e commit 55dd06e

File tree

7 files changed

+52
-5
lines changed

7 files changed

+52
-5
lines changed

packages/better-auth/src/cookies/cookie-utils.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
function tryDecode(str: string): string {
2+
try {
3+
return decodeURIComponent(str);
4+
} catch {
5+
return str;
6+
}
7+
}
8+
19
export interface CookieAttributes {
210
value: string;
311
"max-age"?: number | undefined;
@@ -85,7 +93,8 @@ export function parseSetCookieHeader(
8593
return;
8694
}
8795

88-
const attrObj: CookieAttributes = { value };
96+
const decodedValue = value.includes("%") ? tryDecode(value) : value;
97+
const attrObj: CookieAttributes = { value: decodedValue };
8998

9099
attributes.forEach((attribute) => {
91100
const [attrName, ...attrValueParts] = attribute!.split("=");

packages/better-auth/src/cookies/cookies.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,12 @@ describe("cookie-utils parseSetCookieHeader", () => {
226226
);
227227
});
228228

229+
it("decodes URI-encoded cookie values", () => {
230+
const header = "token=hello%20world%3Dfoo; Path=/";
231+
const map = parseSetCookieHeader(header);
232+
expect(map.get("token")?.value).toBe("hello world=foo");
233+
});
234+
229235
it("handles cookie with Expires followed by cookie without Expires", () => {
230236
const map = parseSetCookieHeader(
231237
"session=xyz; Expires=Mon, 01 Jan 2026 00:00:00 GMT, token=abc",

packages/better-auth/src/integrations/next-js.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export const nextCookies = () => {
100100
path: value.path,
101101
} as const;
102102
try {
103-
cookieHelper.set(key, decodeURIComponent(value.value), opts);
103+
cookieHelper.set(key, value.value, opts);
104104
} catch {
105105
// this will fail if the cookie is being set on server component
106106
}

packages/better-auth/src/integrations/svelte-kit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export const sveltekitCookies = (
7878

7979
for (const [name, { value, ...ops }] of parsed) {
8080
try {
81-
event.cookies.set(name, decodeURIComponent(value), {
81+
event.cookies.set(name, value, {
8282
sameSite: ops.samesite,
8383
path: ops.path || "/",
8484
expires: ops.expires,

packages/better-auth/src/integrations/tanstack-start-solid.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const tanstackStartCookies = () => {
5151
path: value.path,
5252
} as const;
5353
try {
54-
setCookie(key, decodeURIComponent(value.value), opts);
54+
setCookie(key, value.value, opts);
5555
} catch {
5656
// this will fail if the cookie is being set on server component
5757
}

packages/better-auth/src/integrations/tanstack-start.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const tanstackStartCookies = () => {
5151
path: value.path,
5252
} as const;
5353
try {
54-
setCookie(key, decodeURIComponent(value.value), opts);
54+
setCookie(key, value.value, opts);
5555
} catch {
5656
// this will fail if the cookie is being set on server component
5757
}

packages/better-auth/src/plugins/custom-session/custom-session.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, expectTypeOf, it } from "vitest";
22
import { createAuthClient } from "../../client";
3+
import { parseSetCookieHeader } from "../../cookies";
34
import { getTestInstance } from "../../test-utils/test-instance";
45
import type { BetterAuthOptions } from "../../types";
56
import { admin } from "../admin";
@@ -81,6 +82,37 @@ describe("Custom Session Plugin Tests", async () => {
8182
});
8283
});
8384

85+
it("should not double-encode session cookie during get-session refresh", async () => {
86+
const { headers } = await signInWithTestUser();
87+
const signedInCookie = headers.get("cookie");
88+
const signedInSessionToken = signedInCookie?.match(
89+
/better-auth\.session_token=([^;]+)/,
90+
)?.[1];
91+
expect(signedInSessionToken).toBeDefined();
92+
93+
let refreshedSessionToken: string | undefined;
94+
await client.getSession({
95+
fetchOptions: {
96+
headers,
97+
onResponse(context) {
98+
const setCookies = context.response.headers.getSetCookie();
99+
for (const cookieStr of setCookies) {
100+
const parsed = parseSetCookieHeader(cookieStr);
101+
const token = parsed.get("better-auth.session_token")?.value;
102+
if (token) {
103+
refreshedSessionToken = token;
104+
break;
105+
}
106+
}
107+
},
108+
},
109+
});
110+
111+
expect(refreshedSessionToken).toBeDefined();
112+
expect(refreshedSessionToken).toBe(signedInSessionToken);
113+
expect(refreshedSessionToken).not.toContain("%25");
114+
});
115+
84116
it("should return the custom session for multi-session", async () => {
85117
const headers = new Headers();
86118
const testUser = {

0 commit comments

Comments
 (0)