diff --git a/lib/__tests__/verify-jmap-auth.test.ts b/lib/__tests__/verify-jmap-auth.test.ts index efc8dfe8e..665bb27cd 100644 --- a/lib/__tests__/verify-jmap-auth.test.ts +++ b/lib/__tests__/verify-jmap-auth.test.ts @@ -173,3 +173,31 @@ describe('verifyJmapAuth SSRF protection', () => { expect(fetchSpy).not.toHaveBeenCalled(); }); }); + +describe('normalizeJmapServerUrl', () => { + async function load() { + return import('@/lib/auth/verify-jmap-auth'); + } + + it('accepts an app-relative URL (dev mock JMAP server)', async () => { + const { normalizeJmapServerUrl } = await load(); + expect(normalizeJmapServerUrl('/api/dev-jmap')).toBe('/api/dev-jmap'); + }); + + it('strips query, hash, and trailing slashes from an app-relative URL', async () => { + const { normalizeJmapServerUrl } = await load(); + expect(normalizeJmapServerUrl('/api/dev-jmap/?x=1#frag')).toBe('/api/dev-jmap'); + }); + + it('rejects protocol-relative URLs', async () => { + const { normalizeJmapServerUrl } = await load(); + expect(() => normalizeJmapServerUrl('//evil.example.com/api')).toThrow('Invalid server URL'); + }); + + it('normalizes absolute URLs unchanged', async () => { + const { normalizeJmapServerUrl } = await load(); + expect(normalizeJmapServerUrl('https://mail.example.com/jmap/?a=b')).toBe( + 'https://mail.example.com/jmap', + ); + }); +}); diff --git a/lib/auth/verify-jmap-auth.ts b/lib/auth/verify-jmap-auth.ts index 06a557081..f7391c355 100644 --- a/lib/auth/verify-jmap-auth.ts +++ b/lib/auth/verify-jmap-auth.ts @@ -18,6 +18,15 @@ function isSupportedProtocol(protocol: string): boolean { } export function normalizeJmapServerUrl(serverUrl: string): string { + // App-relative URLs (the dev mock JMAP server, e.g. /api/dev-jmap) have no + // host or protocol to validate; they can only ever target this app itself. + // Protocol-relative URLs (//host/...) are NOT app-relative and fall through + // to the absolute-URL parse below, which rejects them. + if (serverUrl.startsWith('/') && !serverUrl.startsWith('//')) { + const url = new URL(serverUrl, 'http://relative.invalid'); + return url.pathname.replace(/\/+$/, ''); + } + let url: URL; try { url = new URL(serverUrl);