From cd7e85b256c4234ed1694f5c44a628cf3ab74fd6 Mon Sep 17 00:00:00 2001 From: Chetan Sharma Date: Wed, 8 Jul 2026 13:08:14 +0530 Subject: [PATCH 1/6] fix(auth0-express): harden createRouteUrl against path injection attacks Strip leading backslashes in addition to forward slashes to prevent WHATWG URL parser normalisation bypasses, and add an explicit scheme check before URL construction to reject javascript:, data:, file: and other scheme-based injection attempts as defence-in-depth. toSafeRedirect is updated to branch on whether input carries a scheme: absolute URLs are validated via origin check directly, while relative paths continue through the strict createRouteUrl validation. --- packages/auth0-express/src/utils.spec.ts | 47 +++++++++++++++++++++--- packages/auth0-express/src/utils.ts | 31 +++++++++------- 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/packages/auth0-express/src/utils.spec.ts b/packages/auth0-express/src/utils.spec.ts index 100a794..f2ad8d0 100644 --- a/packages/auth0-express/src/utils.spec.ts +++ b/packages/auth0-express/src/utils.spec.ts @@ -30,10 +30,25 @@ describe('createRouteUrl', () => { test('throws when path has multiple leading slashes', () => { expect(() => createRouteUrl('///evil.com/auth/callback', BASE)).toThrow( - "Invalid route configuration: '///evil.com/auth/callback' contains multiple leading slashes" + "Invalid route configuration: '///evil.com/auth/callback' contains multiple leading slashes or backslashes" ); expect(() => createRouteUrl('///custom-auth/callback', BASE)).toThrow( - "Invalid route configuration: '///custom-auth/callback' contains multiple leading slashes" + "Invalid route configuration: '///custom-auth/callback' contains multiple leading slashes or backslashes" + ); + }); + + test('throws when path has multiple leading backslashes', () => { + expect(() => createRouteUrl('\\\\evil.com/path', BASE)).toThrow( + "Invalid route configuration: '\\\\evil.com/path' contains multiple leading slashes or backslashes" + ); + }); + + test('throws when path has mixed leading slash and backslash', () => { + expect(() => createRouteUrl('/\\evil.com/path', BASE)).toThrow( + "Invalid route configuration: '/\\evil.com/path' contains multiple leading slashes or backslashes" + ); + expect(() => createRouteUrl('\\/evil.com/path', BASE)).toThrow( + "Invalid route configuration: '\\/evil.com/path' contains multiple leading slashes or backslashes" ); }); @@ -46,16 +61,38 @@ describe('createRouteUrl', () => { }); test('throws when path is an absolute URL with a scheme', () => { - expect(() => createRouteUrl('https://evil.com/auth/callback', BASE)).toThrow(); + expect(() => createRouteUrl('https://evil.com/auth/callback', BASE)).toThrow( + "Invalid route configuration: 'https://evil.com/auth/callback' must not contain a URL scheme" + ); }); test('throws when path uses http scheme to override the base URL origin', () => { - expect(() => createRouteUrl('http://evil.com/auth/callback', BASE)).toThrow(); + expect(() => createRouteUrl('http://evil.com/auth/callback', BASE)).toThrow( + "Invalid route configuration: 'http://evil.com/auth/callback' must not contain a URL scheme" + ); + }); + + test('throws when path uses javascript scheme', () => { + expect(() => createRouteUrl('javascript:alert(1)', BASE)).toThrow( + "Invalid route configuration: 'javascript:alert(1)' must not contain a URL scheme" + ); + }); + + test('throws when path uses data scheme', () => { + expect(() => createRouteUrl('data:text/html,

x

', BASE)).toThrow( + "Invalid route configuration: 'data:text/html,

x

' must not contain a URL scheme" + ); + }); + + test('throws when path uses file scheme', () => { + expect(() => createRouteUrl('file:///etc/passwd', BASE)).toThrow( + "Invalid route configuration: 'file:///etc/passwd' must not contain a URL scheme" + ); }); test('throws for protocol-relative-looking paths with multiple leading slashes', () => { expect(() => createRouteUrl('////evil.com/path', BASE)).toThrow( - "Invalid route configuration: '////evil.com/path' contains multiple leading slashes" + "Invalid route configuration: '////evil.com/path' contains multiple leading slashes or backslashes" ); }); }); diff --git a/packages/auth0-express/src/utils.ts b/packages/auth0-express/src/utils.ts index 9aa220f..f410708 100644 --- a/packages/auth0-express/src/utils.ts +++ b/packages/auth0-express/src/utils.ts @@ -15,13 +15,13 @@ function ensureTrailingSlash(value: string) { } /** - * Ensures the value does not have any leading slashes. + * Ensures the value does not have any leading slashes or backslashes. * If it does, it will trim all of them. - * @param value The value to ensure has no leading slashes. - * @returns The value without leading slashes. + * @param value The value to ensure has no leading slashes or backslashes. + * @returns The value without leading slashes or backslashes. */ function ensureNoLeadingSlash(value: string) { - return value.replace(/^\/+/, ''); + return value.replace(/^[/\\]*/g, ''); } /** @@ -56,8 +56,12 @@ export function createRouteUrl(url: string, base: string) { const baseUrl = new URL(ensureTrailingSlash(base)); const normalized = ensureNoLeadingSlash(url); - if (normalized !== url.replace(/^\//, '')) { - throw new Error(`Invalid route configuration: '${url}' contains multiple leading slashes`); + if (normalized !== url.replace(/^[/\\]/, '')) { + throw new Error(`Invalid route configuration: '${url}' contains multiple leading slashes or backslashes`); + } + + if (/^[a-zA-Z0-9.+-]*:/.test(normalized)) { + throw new Error(`Invalid route configuration: '${url}' must not contain a URL scheme`); } const result = new URL(normalized, baseUrl); @@ -69,24 +73,23 @@ export function createRouteUrl(url: string, base: string) { /** * Function to ensure a redirect URL is safe to use, as in, it has the same origin as the safeBaseUrl. + * Accepts both absolute same-origin URLs and relative paths. * @param dangerousRedirect The redirect URL to check. * @param safeBaseUrl The base URL to check against. * @returns A safe redirect URL or undefined if the redirect URL is not safe. */ export function toSafeRedirect(dangerousRedirect: string, safeBaseUrl: string): string | undefined { - let url: URL; + const safeOrigin = new URL(safeBaseUrl).origin; try { - url = createRouteUrl(dangerousRedirect, safeBaseUrl); + const url = /^[a-zA-Z0-9.+-]*:/.test(dangerousRedirect) + ? new URL(dangerousRedirect) + : createRouteUrl(dangerousRedirect, safeBaseUrl); + + return url.origin === safeOrigin ? url.toString() : undefined; } catch { return undefined; } - - if (url.origin === new URL(safeBaseUrl).origin) { - return url.toString(); - } - - return undefined; } export function createServerClientInstance(options: Auth0Options) { From e85cd747e886c06193ab3b5c43cd94878548e631 Mon Sep 17 00:00:00 2001 From: Chetan Sharma Date: Wed, 8 Jul 2026 17:36:26 +0530 Subject: [PATCH 2/6] fix(auth0-express): address code review findings on path validation hardening - Fix scheme regex from /^[a-zA-Z0-9.+-]*:/ to /^[a-zA-Z][a-zA-Z0-9.+-]*:/ so paths with a digit-start first segment (e.g. /2024-report:summary) or a bare leading colon (:param) are not misidentified as URL schemes - Extract SCHEME_REGEX to a named constant shared by createRouteUrl and toSafeRedirect to prevent validation drift - Fix ensureNoLeadingSlash: change * to + and remove the dead g flag - Move new URL(safeBaseUrl).origin inside the try block in toSafeRedirect so a malformed safeBaseUrl returns undefined instead of throwing uncaught - Replace createRouteUrl(req.url, appBaseUrl) in callback-handler with new URL(req.url, appBaseUrl) + origin check, so absolute-form request targets forwarded by reverse proxies are handled correctly - Add tests for single leading backslash, digit-start colon path, colon in non-first segment, toSafeRedirect backslash cases, and malformed safeBaseUrl --- .../src/handlers/callback-handler.ts | 11 ++++++-- packages/auth0-express/src/utils.spec.ts | 28 +++++++++++++++++++ packages/auth0-express/src/utils.ts | 11 ++++---- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/auth0-express/src/handlers/callback-handler.ts b/packages/auth0-express/src/handlers/callback-handler.ts index e065881..fa3a961 100644 --- a/packages/auth0-express/src/handlers/callback-handler.ts +++ b/packages/auth0-express/src/handlers/callback-handler.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import { createRouteUrl } from '../utils.js'; import { resolveAppBaseUrl } from '../app-base-url.js'; import { Auth0Options } from '../types.js'; @@ -7,8 +6,16 @@ export async function handleCallback(req: Request, res: Response, options: Auth0 try { const appBaseUrl = resolveAppBaseUrl(options.appBaseUrl, req); + // req.url is normally an origin-form path (/auth/callback?...) but can be a full + // absolute URL when the request arrives via a reverse proxy. new URL() handles both; + // any parse error is caught by the outer catch and returned as a 500. + const callbackUrl = new URL(req.url, appBaseUrl); + if (callbackUrl.origin !== new URL(appBaseUrl).origin) { + throw new Error('URL is not allowed: origin does not match base URL'); + } + const { appState } = await req.auth0.client.completeInteractiveLogin<{ returnTo: string } | undefined>( - createRouteUrl(req.url, appBaseUrl) + callbackUrl ); res.redirect(appState?.returnTo ?? appBaseUrl); diff --git a/packages/auth0-express/src/utils.spec.ts b/packages/auth0-express/src/utils.spec.ts index f2ad8d0..3d6f0d8 100644 --- a/packages/auth0-express/src/utils.spec.ts +++ b/packages/auth0-express/src/utils.spec.ts @@ -60,6 +60,21 @@ describe('createRouteUrl', () => { expect(createRouteUrl('auth/callback', BASE).href).toBe('https://myapp.com/auth/callback'); }); + test('single leading backslash is stripped and resolves as a same-origin relative path', () => { + // A single \ is stripped to a plain relative path — not a protocol-relative bypass. + // The WHATWG URL parser also normalises \ to / within the path. + expect(createRouteUrl('\\auth\\callback', BASE).href).toBe('https://myapp.com/auth/callback'); + }); + + test('allows paths with a colon in non-first segment', () => { + expect(createRouteUrl('/a/b:c', BASE).href).toBe('https://myapp.com/a/b:c'); + }); + + test('allows paths whose first segment starts with a digit followed by a colon', () => { + // "2024-report:summary" starts with a digit — not a valid URI scheme per RFC 3986. + expect(createRouteUrl('/2024-report:summary', BASE).href).toBe('https://myapp.com/2024-report:summary'); + }); + test('throws when path is an absolute URL with a scheme', () => { expect(() => createRouteUrl('https://evil.com/auth/callback', BASE)).toThrow( "Invalid route configuration: 'https://evil.com/auth/callback' must not contain a URL scheme" @@ -126,6 +141,19 @@ describe('toSafeRedirect', () => { test('returns undefined for a URL with a different port (different origin)', () => { expect(toSafeRedirect('http://localhost:4000/path', BASE)).toBeUndefined(); }); + + test('returns undefined for double backslash (protocol-relative bypass attempt)', () => { + expect(toSafeRedirect('\\\\evil.com', BASE)).toBeUndefined(); + }); + + test('returns undefined for mixed slash and backslash', () => { + expect(toSafeRedirect('/\\evil.com', BASE)).toBeUndefined(); + expect(toSafeRedirect('\\/evil.com', BASE)).toBeUndefined(); + }); + + test('returns undefined when safeBaseUrl is not a valid URL', () => { + expect(toSafeRedirect('/dashboard', 'not-a-url')).toBeUndefined(); + }); }); describe('wrapDomainResolver', () => { diff --git a/packages/auth0-express/src/utils.ts b/packages/auth0-express/src/utils.ts index f410708..851a800 100644 --- a/packages/auth0-express/src/utils.ts +++ b/packages/auth0-express/src/utils.ts @@ -21,9 +21,11 @@ function ensureTrailingSlash(value: string) { * @returns The value without leading slashes or backslashes. */ function ensureNoLeadingSlash(value: string) { - return value.replace(/^[/\\]*/g, ''); + return value.replace(/^[/\\]+/, ''); } +const SCHEME_REGEX = /^[a-zA-Z][a-zA-Z0-9.+-]*:/; + /** * Wraps a user-provided domain resolver so it always receives the Express * request context. `@auth0/auth0-server-js` invokes the resolver with the @@ -60,7 +62,7 @@ export function createRouteUrl(url: string, base: string) { throw new Error(`Invalid route configuration: '${url}' contains multiple leading slashes or backslashes`); } - if (/^[a-zA-Z0-9.+-]*:/.test(normalized)) { + if (SCHEME_REGEX.test(normalized)) { throw new Error(`Invalid route configuration: '${url}' must not contain a URL scheme`); } @@ -79,10 +81,9 @@ export function createRouteUrl(url: string, base: string) { * @returns A safe redirect URL or undefined if the redirect URL is not safe. */ export function toSafeRedirect(dangerousRedirect: string, safeBaseUrl: string): string | undefined { - const safeOrigin = new URL(safeBaseUrl).origin; - try { - const url = /^[a-zA-Z0-9.+-]*:/.test(dangerousRedirect) + const safeOrigin = new URL(safeBaseUrl).origin; + const url = SCHEME_REGEX.test(dangerousRedirect) ? new URL(dangerousRedirect) : createRouteUrl(dangerousRedirect, safeBaseUrl); From f6e10b6e8340b29cf92ece055bbc66fc42cf7746 Mon Sep 17 00:00:00 2001 From: Chetan Sharma Date: Mon, 13 Jul 2026 10:49:39 +0530 Subject: [PATCH 3/6] fix(auth0-express): fix subpath regression and simplify URL validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit createRouteUrl now accepts absolute same-origin URLs (e.g. from a reverse proxy) by branching on whether the input carries a scheme: absolute inputs are parsed directly and validated by origin check, relative inputs go through the existing slash-stripping and multi-leading check. This fixes the subpath deployment regression where new URL('/auth/callback', base) was dropping the subpath, causing redirect_uri mismatches and invalid_grant errors. callback-handler.ts reverts to using createRouteUrl so subpath handling is preserved and there is a single authoritative URL resolution helper. toSafeRedirect is simplified to new URL(input, base) + origin check — new URL() already handles both absolute and relative inputs natively, making the previous scheme-branching logic redundant. --- .../src/handlers/callback-handler.ts | 11 ++----- packages/auth0-express/src/utils.spec.ts | 30 ++++++++++++------- packages/auth0-express/src/utils.ts | 18 ++++++----- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/packages/auth0-express/src/handlers/callback-handler.ts b/packages/auth0-express/src/handlers/callback-handler.ts index fa3a961..e065881 100644 --- a/packages/auth0-express/src/handlers/callback-handler.ts +++ b/packages/auth0-express/src/handlers/callback-handler.ts @@ -1,4 +1,5 @@ import { Request, Response } from 'express'; +import { createRouteUrl } from '../utils.js'; import { resolveAppBaseUrl } from '../app-base-url.js'; import { Auth0Options } from '../types.js'; @@ -6,16 +7,8 @@ export async function handleCallback(req: Request, res: Response, options: Auth0 try { const appBaseUrl = resolveAppBaseUrl(options.appBaseUrl, req); - // req.url is normally an origin-form path (/auth/callback?...) but can be a full - // absolute URL when the request arrives via a reverse proxy. new URL() handles both; - // any parse error is caught by the outer catch and returned as a 500. - const callbackUrl = new URL(req.url, appBaseUrl); - if (callbackUrl.origin !== new URL(appBaseUrl).origin) { - throw new Error('URL is not allowed: origin does not match base URL'); - } - const { appState } = await req.auth0.client.completeInteractiveLogin<{ returnTo: string } | undefined>( - callbackUrl + createRouteUrl(req.url, appBaseUrl) ); res.redirect(appState?.returnTo ?? appBaseUrl); diff --git a/packages/auth0-express/src/utils.spec.ts b/packages/auth0-express/src/utils.spec.ts index 3d6f0d8..084cb5e 100644 --- a/packages/auth0-express/src/utils.spec.ts +++ b/packages/auth0-express/src/utils.spec.ts @@ -75,33 +75,45 @@ describe('createRouteUrl', () => { expect(createRouteUrl('/2024-report:summary', BASE).href).toBe('https://myapp.com/2024-report:summary'); }); - test('throws when path is an absolute URL with a scheme', () => { + test('accepts an absolute same-origin URL (e.g. from a reverse proxy)', () => { + expect(createRouteUrl('https://myapp.com/auth/callback?code=abc', BASE).href).toBe( + 'https://myapp.com/auth/callback?code=abc' + ); + }); + + test('accepts an absolute same-origin URL with subpath base', () => { + expect(createRouteUrl('https://myapp.com/subapp/auth/callback?code=abc', BASE_WITH_SUBPATH).href).toBe( + 'https://myapp.com/subapp/auth/callback?code=abc' + ); + }); + + test('throws when absolute URL has a different origin', () => { expect(() => createRouteUrl('https://evil.com/auth/callback', BASE)).toThrow( - "Invalid route configuration: 'https://evil.com/auth/callback' must not contain a URL scheme" + 'URL is not allowed: origin does not match base URL' ); }); test('throws when path uses http scheme to override the base URL origin', () => { expect(() => createRouteUrl('http://evil.com/auth/callback', BASE)).toThrow( - "Invalid route configuration: 'http://evil.com/auth/callback' must not contain a URL scheme" + 'URL is not allowed: origin does not match base URL' ); }); test('throws when path uses javascript scheme', () => { expect(() => createRouteUrl('javascript:alert(1)', BASE)).toThrow( - "Invalid route configuration: 'javascript:alert(1)' must not contain a URL scheme" + 'URL is not allowed: origin does not match base URL' ); }); test('throws when path uses data scheme', () => { expect(() => createRouteUrl('data:text/html,

x

', BASE)).toThrow( - "Invalid route configuration: 'data:text/html,

x

' must not contain a URL scheme" + 'URL is not allowed: origin does not match base URL' ); }); test('throws when path uses file scheme', () => { expect(() => createRouteUrl('file:///etc/passwd', BASE)).toThrow( - "Invalid route configuration: 'file:///etc/passwd' must not contain a URL scheme" + 'URL is not allowed: origin does not match base URL' ); }); @@ -120,7 +132,6 @@ describe('toSafeRedirect', () => { }); test('returns undefined for triple-slash path', () => { - // ///evil.com → throws due to multiple leading slashes → caught → returns undefined expect(toSafeRedirect('///evil.com/path', BASE)).toBeUndefined(); }); @@ -142,11 +153,8 @@ describe('toSafeRedirect', () => { expect(toSafeRedirect('http://localhost:4000/path', BASE)).toBeUndefined(); }); - test('returns undefined for double backslash (protocol-relative bypass attempt)', () => { + test('returns undefined for backslash-based protocol-relative bypass attempts', () => { expect(toSafeRedirect('\\\\evil.com', BASE)).toBeUndefined(); - }); - - test('returns undefined for mixed slash and backslash', () => { expect(toSafeRedirect('/\\evil.com', BASE)).toBeUndefined(); expect(toSafeRedirect('\\/evil.com', BASE)).toBeUndefined(); }); diff --git a/packages/auth0-express/src/utils.ts b/packages/auth0-express/src/utils.ts index 851a800..7a9a048 100644 --- a/packages/auth0-express/src/utils.ts +++ b/packages/auth0-express/src/utils.ts @@ -56,16 +56,21 @@ export function wrapDomainResolver( */ export function createRouteUrl(url: string, base: string) { const baseUrl = new URL(ensureTrailingSlash(base)); + + if (SCHEME_REGEX.test(url)) { + const absolute = new URL(url); + if (absolute.origin !== baseUrl.origin) { + throw new Error('URL is not allowed: origin does not match base URL'); + } + return absolute; + } + const normalized = ensureNoLeadingSlash(url); if (normalized !== url.replace(/^[/\\]/, '')) { throw new Error(`Invalid route configuration: '${url}' contains multiple leading slashes or backslashes`); } - if (SCHEME_REGEX.test(normalized)) { - throw new Error(`Invalid route configuration: '${url}' must not contain a URL scheme`); - } - const result = new URL(normalized, baseUrl); if (result.origin !== baseUrl.origin) { throw new Error('URL is not allowed: origin does not match base URL'); @@ -83,10 +88,7 @@ export function createRouteUrl(url: string, base: string) { export function toSafeRedirect(dangerousRedirect: string, safeBaseUrl: string): string | undefined { try { const safeOrigin = new URL(safeBaseUrl).origin; - const url = SCHEME_REGEX.test(dangerousRedirect) - ? new URL(dangerousRedirect) - : createRouteUrl(dangerousRedirect, safeBaseUrl); - + const url = new URL(dangerousRedirect, safeBaseUrl); return url.origin === safeOrigin ? url.toString() : undefined; } catch { return undefined; From 2ae7d9a52502a8f1e8877f452686195a03729706 Mon Sep 17 00:00:00 2001 From: Chetan Sharma Date: Mon, 13 Jul 2026 10:59:11 +0530 Subject: [PATCH 4/6] fix(auth0-express): restore subpath handling in toSafeRedirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplifying toSafeRedirect to new URL(input, base) dropped the subpath when resolving relative paths — new URL('/auth/callback', 'https://myapp.com/subapp') ignores the subpath, producing https://myapp.com/auth/callback instead of https://myapp.com/subapp/auth/callback. Restore the scheme-branching so relative paths go through createRouteUrl which strips the leading slash before resolving against the full base, preserving the subpath. --- packages/auth0-express/src/utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/auth0-express/src/utils.ts b/packages/auth0-express/src/utils.ts index 7a9a048..0c3844c 100644 --- a/packages/auth0-express/src/utils.ts +++ b/packages/auth0-express/src/utils.ts @@ -88,7 +88,9 @@ export function createRouteUrl(url: string, base: string) { export function toSafeRedirect(dangerousRedirect: string, safeBaseUrl: string): string | undefined { try { const safeOrigin = new URL(safeBaseUrl).origin; - const url = new URL(dangerousRedirect, safeBaseUrl); + const url = SCHEME_REGEX.test(dangerousRedirect) + ? new URL(dangerousRedirect) + : createRouteUrl(dangerousRedirect, safeBaseUrl); return url.origin === safeOrigin ? url.toString() : undefined; } catch { return undefined; From 5d3cc1d1d114437a56e7a4bbcfd6d5775a29e270 Mon Sep 17 00:00:00 2001 From: Chetan Sharma Date: Mon, 13 Jul 2026 11:04:59 +0530 Subject: [PATCH 5/6] chore(auth0-express): add comment explaining toSafeRedirect scheme branching --- packages/auth0-express/src/utils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/auth0-express/src/utils.ts b/packages/auth0-express/src/utils.ts index 0c3844c..6df9dc0 100644 --- a/packages/auth0-express/src/utils.ts +++ b/packages/auth0-express/src/utils.ts @@ -88,6 +88,10 @@ export function createRouteUrl(url: string, base: string) { export function toSafeRedirect(dangerousRedirect: string, safeBaseUrl: string): string | undefined { try { const safeOrigin = new URL(safeBaseUrl).origin; + // Absolute URLs are validated by origin check directly. + // Relative paths go through createRouteUrl rather than new URL(input, base) because + // new URL() treats a leading slash as origin-absolute, dropping the subpath in + // subpath deployments. const url = SCHEME_REGEX.test(dangerousRedirect) ? new URL(dangerousRedirect) : createRouteUrl(dangerousRedirect, safeBaseUrl); From 5d84d83c23b6dbf14ec838ab014f3ae7f77d8f56 Mon Sep 17 00:00:00 2001 From: Chetan Sharma Date: Mon, 13 Jul 2026 11:24:10 +0530 Subject: [PATCH 6/6] docs(auth0-express): improve JSDoc comments for createRouteUrl and toSafeRedirect --- packages/auth0-express/src/utils.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/auth0-express/src/utils.ts b/packages/auth0-express/src/utils.ts index 6df9dc0..a50a929 100644 --- a/packages/auth0-express/src/utils.ts +++ b/packages/auth0-express/src/utils.ts @@ -48,11 +48,12 @@ export function wrapDomainResolver( /** * Utility function to ensure Route URLs are created correctly when using both the root and subpath as base URL. + * Accepts a relative path or an absolute same-origin URL, such as one forwarded by a reverse proxy. * Validates that the constructed URL has the same origin as the base URL to prevent host override attacks. - * @param url The URL to use. - * @param base The base URL to use. + * @param url Relative path (e.g. `/auth/callback`) or absolute same-origin URL. + * @param base The base URL to use, including any subpath. * @returns A URL object, combining the base and url. - * @throws {Error} If the constructed URL origin does not match the base URL origin. + * @throws {Error} If the input contains multiple leading slashes/backslashes or the origin does not match the base URL. */ export function createRouteUrl(url: string, base: string) { const baseUrl = new URL(ensureTrailingSlash(base)); @@ -79,11 +80,12 @@ export function createRouteUrl(url: string, base: string) { } /** - * Function to ensure a redirect URL is safe to use, as in, it has the same origin as the safeBaseUrl. - * Accepts both absolute same-origin URLs and relative paths. - * @param dangerousRedirect The redirect URL to check. + * Validates a user-supplied redirect URL (e.g. a `returnTo` query parameter) against the application base URL. + * Accepts relative paths and absolute same-origin URLs. Rejects different-origin URLs and injection attempts. + * Never throws — returns undefined for any invalid or unsafe input. + * @param dangerousRedirect Untrusted redirect URL from user input. * @param safeBaseUrl The base URL to check against. - * @returns A safe redirect URL or undefined if the redirect URL is not safe. + * @returns A safe redirect URL, or undefined if the input is not safe. */ export function toSafeRedirect(dangerousRedirect: string, safeBaseUrl: string): string | undefined { try {