From cbf5d83a693d0328282ddb0f5d398c351d92558e Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Sun, 23 Aug 2026 12:49:11 -0500 Subject: [PATCH] fix: maintentance return URL sanitization (#30948) --- web/src/lib/utils/maintenance.spec.ts | 27 +++++++++++++++++++++++++++ web/src/lib/utils/maintenance.ts | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 web/src/lib/utils/maintenance.spec.ts diff --git a/web/src/lib/utils/maintenance.spec.ts b/web/src/lib/utils/maintenance.spec.ts new file mode 100644 index 0000000000..89cf7e12da --- /dev/null +++ b/web/src/lib/utils/maintenance.spec.ts @@ -0,0 +1,27 @@ +import { maintenanceReturnUrl } from '$lib/utils/maintenance'; + +describe('maintenance', () => { + describe(maintenanceReturnUrl.name, () => { + beforeEach(() => { + // @ts-expect-error - override location for testing + // eslint-disable-next-line unicorn/no-global-object-property-assignment + globalThis.location = new URL('https://my.immich.server'); + vi.spyOn(document, 'baseURI', 'get').mockReturnValue('https://my.immich.server/'); + }); + + it('should resolve a same-origin continue url', () => { + expect(maintenanceReturnUrl(new URLSearchParams({ continue: '/photos' }))).property( + 'href', + 'https://my.immich.server/photos', + ); + }); + + it('should fall back to the root route when continue is missing', () => { + expect(maintenanceReturnUrl(new URLSearchParams())).property('href', 'https://my.immich.server/'); + }); + + it('should reject a cross-origin continue url', () => { + expect(maintenanceReturnUrl(new URLSearchParams({ continue: 'https://malicious.site/evil' }))).toBe('/'); + }); + }); +}); diff --git a/web/src/lib/utils/maintenance.ts b/web/src/lib/utils/maintenance.ts index 133b3d8f1f..317180f884 100644 --- a/web/src/lib/utils/maintenance.ts +++ b/web/src/lib/utils/maintenance.ts @@ -8,7 +8,7 @@ export function maintenanceCreateUrl(url: URL) { } export function maintenanceReturnUrl(searchParams: URLSearchParams) { - return searchParams.get('continue') ?? '/'; + return Route.continue(searchParams.get('continue'), '/'); } export function maintenanceShouldRedirect(maintenanceMode: boolean, currentUrl: URL | Location) {