From ac05284481dc469044d9dce40d560df6b49a6d1e Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 14 Aug 2026 18:50:00 -0700 Subject: [PATCH] fix(browse): allow about:blank so a restarted daemon can initialise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daemon opens its own first tab on about:blank, so blocking it in validateNavigationUrl meant a restarted daemon could never recreate the blank tab it starts from — and `browse newtab about:blank`, which `make-pdf setup` runs as its Chromium smoke test, failed and surfaced as "Chromium failed to launch" against a healthy browser. Allow about:blank ONLY, never the about: scheme: about:blank has no origin, loads nothing and runs nothing, while about:config and friends are real surfaces. Exact href match (lower-cased, since the URL parser normalises the protocol but not the opaque part), so about:blankfoo stays blocked. Contributed by @jwilk-hrep (PR #2537). Co-Authored-By: Claude Fable 5 --- browse/src/url-validation.ts | 17 ++++++++++++++++- browse/test/url-validation.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) 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); });