Skip to content

Commit fc33a79

Browse files
authored
fix: consistently treat returnTo parameter as an absolute path (#2185)
1 parent 3d0f19e commit fc33a79

File tree

4 files changed

+47
-18
lines changed

4 files changed

+47
-18
lines changed

src/server/auth-client.test.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,7 +1552,7 @@ ca/T0LLtgmbMmxSv/MmzIg==
15521552
codeVerifier: expect.any(String),
15531553
responseType: "code",
15541554
state: authorizationUrl.searchParams.get("state"),
1555-
returnTo: "https://example.com/dashboard"
1555+
returnTo: "/dashboard"
15561556
})
15571557
);
15581558
});
@@ -4851,8 +4851,27 @@ ca/T0LLtgmbMmxSv/MmzIg==
48514851
// Mock the transactionStore.save method to verify the saved state
48524852
const originalSave = authClient["transactionStore"].save;
48534853
authClient["transactionStore"].save = vi.fn(async (cookies, state) => {
4854-
// The full URL is saved, not just the path
4855-
expect(state.returnTo).toBe("https://example.com/custom-return-path");
4854+
expect(state.returnTo).toBe("/custom-return-path");
4855+
return originalSave.call(
4856+
authClient["transactionStore"],
4857+
cookies,
4858+
state
4859+
);
4860+
});
4861+
4862+
await authClient.startInteractiveLogin({ returnTo });
4863+
4864+
expect(authClient["transactionStore"].save).toHaveBeenCalled();
4865+
});
4866+
4867+
it("should sanitize and use the provided returnTo parameter — absolute URL", async () => {
4868+
const authClient = await createAuthClient();
4869+
const returnTo =
4870+
DEFAULT.appBaseUrl + "/custom-return-path?query=param#hash";
4871+
4872+
const originalSave = authClient["transactionStore"].save;
4873+
authClient["transactionStore"].save = vi.fn(async (cookies, state) => {
4874+
expect(state.returnTo).toBe("/custom-return-path?query=param#hash");
48564875
return originalSave.call(
48574876
authClient["transactionStore"],
48584877
cookies,
@@ -4957,7 +4976,7 @@ ca/T0LLtgmbMmxSv/MmzIg==
49574976
codeVerifier: expect.any(String),
49584977
responseType: "code",
49594978
state: expect.any(String),
4960-
returnTo: "https://example.com/custom-path"
4979+
returnTo: "/custom-path"
49614980
})
49624981
);
49634982
return originalSave.call(

src/server/auth-client.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,10 @@ export interface AuthClientOptions {
140140
noContentProfileResponseWhenUnauthenticated?: boolean;
141141
}
142142

143-
function createRouteUrl(url: string, base: string) {
143+
function createRouteUrl(path: string, baseUrl: string) {
144144
return new URL(
145-
ensureNoLeadingSlash(normalizeWithBasePath(url)),
146-
ensureTrailingSlash(base)
145+
ensureNoLeadingSlash(normalizeWithBasePath(path)),
146+
ensureTrailingSlash(baseUrl)
147147
);
148148
}
149149

@@ -328,7 +328,10 @@ export class AuthClient {
328328
const sanitizedReturnTo = toSafeRedirect(options.returnTo, safeBaseUrl);
329329

330330
if (sanitizedReturnTo) {
331-
returnTo = sanitizedReturnTo;
331+
returnTo =
332+
sanitizedReturnTo.pathname +
333+
sanitizedReturnTo.search +
334+
sanitizedReturnTo.hash;
332335
}
333336
}
334337

src/utils/url-helpers.test.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,34 @@ describe("url-helpers", () => {
1414
});
1515

1616
it("should allow relative urls", () => {
17-
expect(toSafeRedirect("/foo", safeBaseUrl)).toEqual(
17+
expect(toSafeRedirect("/foo", safeBaseUrl)?.toString()).toEqual(
1818
"http://www.example.com/foo"
1919
);
20-
expect(toSafeRedirect("foo", safeBaseUrl)).toEqual(
20+
expect(toSafeRedirect("foo", safeBaseUrl)?.toString()).toEqual(
2121
"http://www.example.com/foo"
2222
);
23-
expect(toSafeRedirect("/foo?some=value", safeBaseUrl)).toEqual(
24-
"http://www.example.com/foo?some=value"
25-
);
2623
expect(
27-
toSafeRedirect("/foo?someUrl=https://www.google.com", safeBaseUrl)
24+
toSafeRedirect("/foo?some=value", safeBaseUrl)?.toString()
25+
).toEqual("http://www.example.com/foo?some=value");
26+
expect(
27+
toSafeRedirect(
28+
"/foo?someUrl=https://www.google.com",
29+
safeBaseUrl
30+
)?.toString()
2831
).toEqual("http://www.example.com/foo?someUrl=https://www.google.com");
2932
expect(
30-
toSafeRedirect("/foo", new URL("http://www.example.com:8888"))
33+
toSafeRedirect(
34+
"/foo",
35+
new URL("http://www.example.com:8888")
36+
)?.toString()
3137
).toEqual("http://www.example.com:8888/foo");
3238
});
3339

3440
it("should prevent open redirects", () => {
3541
for (const payload of payloads) {
3642
expect(
37-
toSafeRedirect(payload, safeBaseUrl) || safeBaseUrl.toString()
43+
toSafeRedirect(payload, safeBaseUrl)?.toString() ||
44+
safeBaseUrl.toString()
3845
).toMatch(/^http:\/\/www\.example\.com\//);
3946
}
4047
});

src/utils/url-helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export function toSafeRedirect(
22
dangerousRedirect: string,
33
safeBaseUrl: URL
4-
): string | undefined {
4+
): URL | undefined {
55
let url: URL;
66

77
try {
@@ -11,7 +11,7 @@ export function toSafeRedirect(
1111
}
1212

1313
if (url.origin === safeBaseUrl.origin) {
14-
return url.toString();
14+
return url;
1515
}
1616

1717
return undefined;

0 commit comments

Comments
 (0)