From c33705b4f9deb675b160332e3dbdce7a68f21d61 Mon Sep 17 00:00:00 2001 From: Jake Wilk Date: Wed, 12 Aug 2026 16:36:28 -0400 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 `validateNavigationUrl` rejected every non-http(s)/file scheme, including `about:blank`. But the daemon opens its own first tab on about:blank, so once it restarted it could never come back: $ browse newtab about:blank Blocked: scheme "about:" is not allowed. Only http:, https:, and file: URLs are permitted. The visible damage is in make-pdf, whose Chromium smoke test is exactly that command. `make-pdf setup` reports: [2/5] Launching Chromium... FAIL Chromium failed to launch: browse newtab exited 1 against a completely healthy Chromium, and generate fails downstream with `page.pdf: Protocol error (IO.read): Read failed` — talking to a daemon that never finished starting. Both symptoms point away from the actual cause, which is why this took a while to find. It only reproduces once the daemon restarts. A daemon still holding its original tab keeps working, so the bug hides until something recycles the process, and then make-pdf is dead until the machine is rebooted or the tab is recreated by some other means. Scoped as narrowly as it can be: about:blank EXACTLY, matched on href, not the `about:` scheme. about:blank has no origin, loads nothing and runs nothing; about:config and about:net-internals are real surfaces and stay blocked, as does about:blankfoo — this is not a prefix test. Compared lower-cased because the URL parser normalises the protocol but not the opaque part, so `ABOUT:BLANK` parses to href `about:BLANK`. Caught by the test rather than by reading the spec. Tests: 4 added — about:blank resolves, case-insensitively; about:config and about:net-internals still rejected; about:blankfoo still rejected. Note for reviewers on Windows: browse/test/url-validation.test.ts has 5 pre-existing failures on this platform, all in the file:// suites, which assume POSIX paths (/tmp, /etc/passwd). Unrelated to this change and unchanged by it — same 5 before and after. --- 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); });