diff --git a/web/src/lib/route.spec.ts b/web/src/lib/route.spec.ts index c00c46848b..30cc183f31 100644 --- a/web/src/lib/route.spec.ts +++ b/web/src/lib/route.spec.ts @@ -47,4 +47,39 @@ describe('Route', () => { expect(Route.systemSettings({ isOpen: OpenQueryParam.OAUTH })).toBe('/admin/system-settings?isOpen=oauth'); }); }); + + describe(Route.continue.name, () => { + beforeEach(() => { + // @ts-expect-error - override location for testing + globalThis.location = new URL('https://my.immich.server'); + vi.spyOn(document, 'baseURI', 'get').mockReturnValue('https://my.immich.server/'); + }); + + it('should resolve relative URLs', () => { + expect(Route.continue('/some/path', '/fallback')).property('href', 'https://my.immich.server/some/path'); + }); + + it('should resolve absolute URLs on the same origin', () => { + expect(Route.continue('https://my.immich.server/some/path', '/fallback')).property( + 'href', + 'https://my.immich.server/some/path', + ); + }); + + it('should return fallback for absolute URLs on a different origin', () => { + expect(Route.continue('https://malicious.site/evil', '/fallback')).toBe('/fallback'); + }); + + it('should return fallback for null URLs', () => { + expect(Route.continue(null, '/fallback')).property('href', 'https://my.immich.server/fallback'); + }); + + it('should block javascript: URLs', () => { + expect(Route.continue('javascript:alert(1)', '/fallback')).toBe('/fallback'); + }); + + it(String.raw`should block \/ URLs`, () => { + expect(Route.continue(String.raw`\/malicious.com`, '/fallback')).toBe('/fallback'); + }); + }); }); diff --git a/web/src/lib/route.ts b/web/src/lib/route.ts index 1846f29796..80c52c544e 100644 --- a/web/src/lib/route.ts +++ b/web/src/lib/route.ts @@ -154,11 +154,13 @@ export const Route = { viewQueue: ({ name }: { name: QueueName }) => `/admin/queues/${asQueueSlug(name)}`, // continue helper for ensuring same-origin URLs - continue: (url: string | null, fallback: string) => { - if (!url || !url.startsWith('/') || url.startsWith('//')) { + continue: (url: string | null, fallback: string): string | URL => { + const resolved = new URL(url ?? fallback, document.baseURI); + + if (resolved.origin !== location.origin) { return fallback; } - return url; + return resolved; }, };