Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-allowed-redirect-origins-port.md
Original file line number Diff line number Diff line change
@@ -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`.
11 changes: 11 additions & 0 deletions packages/shared/src/internal/clerk-js/__tests__/url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,17 @@ 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)
// 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],
];

const warnMock = vi.spyOn(logger, 'warnOnce');
Expand Down
22 changes: 18 additions & 4 deletions packages/shared/src/internal/clerk-js/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,11 @@ export function requiresUserInput(redirectUrl: string): boolean {
}

export const isAllowedRedirect =
(allowedRedirectOrigins: Array<string | RegExp> | undefined, currentOrigin: string) => (_url: URL | string) => {
(
allowedRedirectOrigins: Array<string | RegExp> | undefined,
currentOrigin: string,
): ((_url: URL | string) => boolean) =>
(_url: URL | string): boolean => {
let url = _url;
if (typeof url === 'string') {
url = relativeToAbsoluteUrl(url, currentOrigin);
Expand All @@ -436,12 +440,22 @@ export const isAllowedRedirect =

const isSameOrigin = currentOrigin === url.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 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 ||
allowedRedirectOrigins
.map(origin => (typeof origin === 'string' ? globs.toRegexp(trimTrailingSlash(origin)) : origin))
.some(origin => origin.test(trimTrailingSlash(url.origin))));
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(
Expand Down