From f691851d320bf7d9bdc439a8580b57f096c63b3d Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 22 Jun 2026 10:34:26 +0200 Subject: [PATCH] Merge commit from fork --- apps/login/src/lib/client-utils.ts | 10 +++++-- apps/login/src/lib/oidc.test.ts | 48 ++++++++++++++++++++++++++++++ apps/login/src/lib/oidc.ts | 5 +++- apps/login/src/lib/saml.ts | 5 +++- 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/apps/login/src/lib/client-utils.ts b/apps/login/src/lib/client-utils.ts index 4e1230554..f96e417f9 100644 --- a/apps/login/src/lib/client-utils.ts +++ b/apps/login/src/lib/client-utils.ts @@ -16,8 +16,14 @@ export function handleServerActionResponse( } if ("redirect" in response && response.redirect) { - router.push(response.redirect); - return true; + if (isSafeRedirectUri(response.redirect)) { + router.push(response.redirect); + return true; + } else { + console.warn("handleServerActionResponse: Blocked unsafe redirect URI:", response.redirect); + setError("Unsafe redirect URI was blocked"); + return true; + } } if ("samlData" in response && response.samlData) { diff --git a/apps/login/src/lib/oidc.test.ts b/apps/login/src/lib/oidc.test.ts index 32b522d29..f192a0ff1 100644 --- a/apps/login/src/lib/oidc.test.ts +++ b/apps/login/src/lib/oidc.test.ts @@ -188,4 +188,52 @@ describe("loginWithOIDCAndSession", () => { expect(result).toEqual({ error: "Unknown error occurred" }); }); + + it("should reject javascript: defaultRedirectUri in FailedPrecondition path (XSS prevention)", async () => { + const { ConnectError } = await import("@zitadel/client"); + vi.mocked(sessionModule.isSessionValid).mockResolvedValue(true); + vi.mocked(zitadelModule.createCallback).mockRejectedValue(new ConnectError("already handled", 9)); + vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({ + defaultRedirectUri: "javascript:alert(document.cookie)", + } as any); + + const result = await loginWithOIDCAndSession({ + serviceUrl: mockServiceUrl, + authRequest: mockAuthRequest, + sessionId: mockSessionId, + sessions: mockSessions, + sessionCookies: mockCookies, + }); + + // Should NOT redirect to the javascript: URI — must fall back to /signedin + expect(result).toHaveProperty("redirect"); + if ("redirect" in result) { + expect(result.redirect).not.toContain("javascript:"); + expect(result.redirect).toContain("/signedin"); + } + }); + + it("should reject data: defaultRedirectUri in FailedPrecondition path (XSS prevention)", async () => { + const { ConnectError } = await import("@zitadel/client"); + vi.mocked(sessionModule.isSessionValid).mockResolvedValue(true); + vi.mocked(zitadelModule.createCallback).mockRejectedValue(new ConnectError("already handled", 9)); + vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({ + defaultRedirectUri: "data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==", + } as any); + + const result = await loginWithOIDCAndSession({ + serviceUrl: mockServiceUrl, + authRequest: mockAuthRequest, + sessionId: mockSessionId, + sessions: mockSessions, + sessionCookies: mockCookies, + }); + + // Should NOT redirect to the data: URI — must fall back to /signedin + expect(result).toHaveProperty("redirect"); + if ("redirect" in result) { + expect(result.redirect).not.toContain("data:"); + expect(result.redirect).toContain("/signedin"); + } + }); }); diff --git a/apps/login/src/lib/oidc.ts b/apps/login/src/lib/oidc.ts index e9ade7ab2..d6ce17b6f 100644 --- a/apps/login/src/lib/oidc.ts +++ b/apps/login/src/lib/oidc.ts @@ -1,3 +1,4 @@ +import { isSafeRedirectUri } from "@/lib/client-utils"; import { Cookie } from "@/lib/cookies"; import { isClassifiedError } from "@/lib/grpc/interceptors/error-classification"; import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; @@ -77,8 +78,10 @@ export async function loginWithOIDCAndSession({ organization: selectedSession.factors?.user?.organizationId, }); - if (loginSettings?.defaultRedirectUri) { + if (loginSettings?.defaultRedirectUri && isSafeRedirectUri(loginSettings.defaultRedirectUri)) { return { redirect: loginSettings.defaultRedirectUri }; + } else if (loginSettings?.defaultRedirectUri) { + console.warn("loginWithOIDCAndSession: Unsafe defaultRedirectUri prevented:", loginSettings.defaultRedirectUri); } const signedinUrl = "/signedin"; diff --git a/apps/login/src/lib/saml.ts b/apps/login/src/lib/saml.ts index 96b8eb4d0..1a5ea4a97 100644 --- a/apps/login/src/lib/saml.ts +++ b/apps/login/src/lib/saml.ts @@ -1,3 +1,4 @@ +import { isSafeRedirectUri } from "@/lib/client-utils"; import { Cookie } from "@/lib/cookies"; import { isClassifiedError } from "@/lib/grpc/interceptors/error-classification"; import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; @@ -96,8 +97,10 @@ export async function loginWithSAMLAndSession({ organization: selectedSession.factors?.user?.organizationId, }); - if (loginSettings?.defaultRedirectUri) { + if (loginSettings?.defaultRedirectUri && isSafeRedirectUri(loginSettings.defaultRedirectUri)) { return { redirect: loginSettings.defaultRedirectUri }; + } else if (loginSettings?.defaultRedirectUri) { + console.warn("loginWithSAMLAndSession: Unsafe defaultRedirectUri prevented:", loginSettings.defaultRedirectUri); } const signedinUrl = "/signedin";