From add60f667af3c12aeca5566f3ea3ac4b9364ba27 Mon Sep 17 00:00:00 2001 From: FranKaddour Date: Wed, 1 Jul 2026 09:53:35 -0300 Subject: [PATCH 1/4] fix(shared): allow satellite redirect URLs with non-default ports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isAllowedRedirect() compares url.origin against allowedRedirectOrigins patterns. When the redirect URL includes a non-default port (e.g. :5173 from a Vite dev server), url.origin includes the port suffix, while patterns like 'https://*.example.net' have none — causing the match to fail and the redirect to fall back to the home URL silently. Fix: when url.port is non-empty, also test the port-stripped origin (protocol + hostname) against each pattern. Domain validation is preserved — only the port suffix is relaxed. Fixes #8263 --- .../internal/clerk-js/__tests__/url.test.ts | 8 ++++++++ packages/shared/src/internal/clerk-js/url.ts | 18 +++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/internal/clerk-js/__tests__/url.test.ts b/packages/shared/src/internal/clerk-js/__tests__/url.test.ts index 63927fae344..fc8da335fcd 100644 --- a/packages/shared/src/internal/clerk-js/__tests__/url.test.ts +++ b/packages/shared/src/internal/clerk-js/__tests__/url.test.ts @@ -579,6 +579,14 @@ describe('isAllowedRedirect', () => { ['https:evil.com', [/https:\/\/www\.clerk\.com/], false], ['//evil.com', [/https:\/\/www\.clerk\.com/], false], ['..//evil.com', ['https://www.clerk.com'], false], + // non-default ports — satellite apps in local dev (e.g. Vite on :5173) + ['https://www.clerk.com:3000', ['https://www.clerk.com'], true], + ['https://www.clerk.com:3000/path?q=1', ['https://www.clerk.com'], true], + ['https://app.example.net:5173', ['https://*.example.net'], true], + ['https://app.example.net:5176', ['https://*.example.net'], true], + // non-default port must not bypass domain validation + ['https://evil.com:5173', ['https://*.clerk.com'], false], + ['https://evil.clerk.com.evil.com:5173', ['https://*.clerk.com'], false], ]; const warnMock = vi.spyOn(logger, 'warnOnce'); diff --git a/packages/shared/src/internal/clerk-js/url.ts b/packages/shared/src/internal/clerk-js/url.ts index 6c524febf4a..99cbc2c5c54 100644 --- a/packages/shared/src/internal/clerk-js/url.ts +++ b/packages/shared/src/internal/clerk-js/url.ts @@ -436,12 +436,24 @@ export const isAllowedRedirect = const isSameOrigin = currentOrigin === url.origin; + const patterns = allowedRedirectOrigins.map(origin => + typeof origin === 'string' ? globs.toRegexp(trimTrailingSlash(origin)) : origin, + ); + + // When the redirect URL includes a non-default port (common in local dev, e.g. :5173), + // url.origin includes the port (https://app.example.net:5173) while allowedRedirectOrigins + // patterns are typically port-less (https://*.example.net). Test both forms so that satellite + // apps running on custom ports are not incorrectly rejected. + const portlessOrigin = url.port !== '' ? `${url.protocol}//${url.hostname}` : null; + const isAllowed = !isProblematicUrl(url) && (isSameOrigin || - allowedRedirectOrigins - .map(origin => (typeof origin === 'string' ? globs.toRegexp(trimTrailingSlash(origin)) : origin)) - .some(origin => origin.test(trimTrailingSlash(url.origin)))); + patterns.some( + pattern => + pattern.test(trimTrailingSlash(url.origin)) || + (portlessOrigin !== null && pattern.test(portlessOrigin)), + )); if (!isAllowed) { logger.warnOnce( From ff55231125d2734ee403438c2d9b45aef86f75e8 Mon Sep 17 00:00:00 2001 From: FranKaddour Date: Wed, 1 Jul 2026 09:54:25 -0300 Subject: [PATCH 2/4] chore: add changeset for allowed-redirect-origins port fix --- .changeset/fix-allowed-redirect-origins-port.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-allowed-redirect-origins-port.md diff --git a/.changeset/fix-allowed-redirect-origins-port.md b/.changeset/fix-allowed-redirect-origins-port.md new file mode 100644 index 00000000000..f798c3583c8 --- /dev/null +++ b/.changeset/fix-allowed-redirect-origins-port.md @@ -0,0 +1,5 @@ +--- +"@clerk/shared": patch +--- + +Fix satellite domain redirect validation incorrectly rejecting URLs with non-default ports. Redirect URLs like `https://app.example.net:5173` (common in local development with Vite or similar dev servers) are now correctly matched against `allowedRedirectOrigins` patterns such as `https://*.example.net`. From eea968cb0ce8a78fc7aec52a3edbb5d2abec6dd9 Mon Sep 17 00:00:00 2001 From: FranKaddour Date: Wed, 1 Jul 2026 17:03:58 -0300 Subject: [PATCH 3/4] fix(shared): scope portless redirect fallback to glob entries only Exact string entries in allowedRedirectOrigins (e.g. https://www.clerk.com) were also matching ports they never declared because the portless-origin test ran against all patterns. Port is part of the browser origin, so an exact entry must remain port-sensitive. The portless fallback now only applies when the original entry is a glob (contains '*'), which covers the satellite-app use case where the pattern is https://*.example.net and the dev server runs on :5173. --- .../internal/clerk-js/__tests__/url.test.ts | 7 ++++-- packages/shared/src/internal/clerk-js/url.ts | 22 +++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/shared/src/internal/clerk-js/__tests__/url.test.ts b/packages/shared/src/internal/clerk-js/__tests__/url.test.ts index fc8da335fcd..84f7abe09df 100644 --- a/packages/shared/src/internal/clerk-js/__tests__/url.test.ts +++ b/packages/shared/src/internal/clerk-js/__tests__/url.test.ts @@ -580,10 +580,13 @@ describe('isAllowedRedirect', () => { ['//evil.com', [/https:\/\/www\.clerk\.com/], false], ['..//evil.com', ['https://www.clerk.com'], false], // non-default ports — satellite apps in local dev (e.g. Vite on :5173) - ['https://www.clerk.com:3000', ['https://www.clerk.com'], true], - ['https://www.clerk.com:3000/path?q=1', ['https://www.clerk.com'], true], + // wildcard glob entries accept any port on the matching host ['https://app.example.net:5173', ['https://*.example.net'], true], ['https://app.example.net:5176', ['https://*.example.net'], true], + ['https://www.clerk.com:3000', ['https://*.clerk.com'], true], + // exact string entries remain port-sensitive (port is part of the origin) + ['https://www.clerk.com:3000', ['https://www.clerk.com'], false], + ['https://www.clerk.com:3000/path?q=1', ['https://www.clerk.com'], false], // non-default port must not bypass domain validation ['https://evil.com:5173', ['https://*.clerk.com'], false], ['https://evil.clerk.com.evil.com:5173', ['https://*.clerk.com'], false], diff --git a/packages/shared/src/internal/clerk-js/url.ts b/packages/shared/src/internal/clerk-js/url.ts index 99cbc2c5c54..15f11961dae 100644 --- a/packages/shared/src/internal/clerk-js/url.ts +++ b/packages/shared/src/internal/clerk-js/url.ts @@ -436,24 +436,22 @@ export const isAllowedRedirect = const isSameOrigin = currentOrigin === url.origin; - const patterns = allowedRedirectOrigins.map(origin => - typeof origin === 'string' ? globs.toRegexp(trimTrailingSlash(origin)) : origin, - ); - // When the redirect URL includes a non-default port (common in local dev, e.g. :5173), - // url.origin includes the port (https://app.example.net:5173) while allowedRedirectOrigins - // patterns are typically port-less (https://*.example.net). Test both forms so that satellite - // apps running on custom ports are not incorrectly rejected. + // url.origin includes the port (https://app.example.net:5173) while glob entries like + // https://*.example.net have none. For wildcard/glob entries only, also test the + // port-stripped origin so satellite apps on custom ports are not rejected. + // Exact string entries (no wildcard) remain port-sensitive to preserve the security boundary. const portlessOrigin = url.port !== '' ? `${url.protocol}//${url.hostname}` : null; const isAllowed = !isProblematicUrl(url) && (isSameOrigin || - patterns.some( - pattern => - pattern.test(trimTrailingSlash(url.origin)) || - (portlessOrigin !== null && pattern.test(portlessOrigin)), - )); + allowedRedirectOrigins.some(origin => { + const pattern = typeof origin === 'string' ? globs.toRegexp(trimTrailingSlash(origin)) : origin; + if (pattern.test(trimTrailingSlash(url.origin))) return true; + const isGlobEntry = typeof origin === 'string' && origin.includes('*'); + return isGlobEntry && portlessOrigin !== null && pattern.test(portlessOrigin); + })); if (!isAllowed) { logger.warnOnce( From e25be4d578be206e273615b8e6db6649566e5f36 Mon Sep 17 00:00:00 2001 From: FranKaddour Date: Wed, 1 Jul 2026 17:19:23 -0300 Subject: [PATCH 4/4] fix(shared): add explicit return types to isAllowedRedirect curried function --- packages/shared/src/internal/clerk-js/url.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/internal/clerk-js/url.ts b/packages/shared/src/internal/clerk-js/url.ts index 15f11961dae..805eb2c4d11 100644 --- a/packages/shared/src/internal/clerk-js/url.ts +++ b/packages/shared/src/internal/clerk-js/url.ts @@ -424,7 +424,11 @@ export function requiresUserInput(redirectUrl: string): boolean { } export const isAllowedRedirect = - (allowedRedirectOrigins: Array | undefined, currentOrigin: string) => (_url: URL | string) => { + ( + allowedRedirectOrigins: Array | undefined, + currentOrigin: string, + ): ((_url: URL | string) => boolean) => + (_url: URL | string): boolean => { let url = _url; if (typeof url === 'string') { url = relativeToAbsoluteUrl(url, currentOrigin);