diff --git a/browse/src/url-validation.ts b/browse/src/url-validation.ts index 02992f4bf..c9e9c170a 100644 --- a/browse/src/url-validation.ts +++ b/browse/src/url-validation.ts @@ -269,9 +269,24 @@ export async function validateNavigationUrl(url: string): Promise { return pathToFileURL(fsPath).href + parsed.search + parsed.hash; } + // about:blank ONLY — the canonical empty page, and the one the daemon opens its own + // first tab on. Blocking it meant `browse newtab about:blank` failed, which is what + // `make-pdf setup` runs as its Chromium smoke test: make-pdf reported "Chromium failed + // to launch" against a perfectly healthy Chromium, and any browse session whose daemon + // restarted could never recreate the blank tab it starts from. + // + // Deliberately not the whole `about:` scheme. about:blank has no origin, loads nothing + // and runs nothing; about:config, about:net-internals and friends are real surfaces. + // Exact href match, not a prefix test, so `about:blankfoo` stays blocked. + // Compared lower-cased: the URL parser normalises the PROTOCOL but not the opaque part, + // so `ABOUT:BLANK` parses to href `about:BLANK` and an exact === would reject it. + if (parsed.protocol === 'about:' && parsed.href.toLowerCase() === 'about:blank') { + return 'about:blank'; + } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { throw new Error( - `Blocked: scheme "${parsed.protocol}" is not allowed. Only http:, https:, and file: URLs are permitted.` + `Blocked: scheme "${parsed.protocol}" is not allowed. Only http:, https:, file:, and about:blank URLs are permitted.` ); } diff --git a/browse/test/url-validation.test.ts b/browse/test/url-validation.test.ts index 8f4ab6962..7f7fe5949 100644 --- a/browse/test/url-validation.test.ts +++ b/browse/test/url-validation.test.ts @@ -47,6 +47,28 @@ describe('validateNavigationUrl', () => { await expect(validateNavigationUrl('file://host.example.com/foo.html')).rejects.toThrow(/Unsupported file URL host/i); }); + // The daemon opens its own first tab on about:blank, so blocking it meant a restarted + // daemon could never initialise — and `make-pdf setup`, whose Chromium smoke test is + // `browse newtab about:blank`, reported "Chromium failed to launch" on a healthy browser. + it('allows about:blank — the daemon opens its own first tab there', async () => { + await expect(validateNavigationUrl('about:blank')).resolves.toBe('about:blank'); + }); + + it('allows about:blank regardless of case, since URL parsing normalises it', async () => { + await expect(validateNavigationUrl('ABOUT:BLANK')).resolves.toBe('about:blank'); + }); + + // The allowance is about:blank EXACTLY, not the about: scheme. about:blank has no + // origin and loads nothing; the rest of the scheme is a real surface. + it('still blocks other about: URLs', async () => { + await expect(validateNavigationUrl('about:config')).rejects.toThrow(/scheme.*not allowed/i); + await expect(validateNavigationUrl('about:net-internals')).rejects.toThrow(/scheme.*not allowed/i); + }); + + it('blocks about:blankfoo — exact match, never a prefix test', async () => { + await expect(validateNavigationUrl('about:blankfoo')).rejects.toThrow(/scheme.*not allowed/i); + }); + it('blocks javascript: scheme', async () => { await expect(validateNavigationUrl('javascript:alert(1)')).rejects.toThrow(/scheme.*not allowed/i); });