fix(browse): allow about:blank so a restarted daemon can initialise

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 <noreply@anthropic.com>
This commit is contained in:
Garry Tan 2026-08-14 18:50:00 -07:00
parent 29a717211a
commit ac05284481
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
2 changed files with 38 additions and 1 deletions

View File

@ -269,9 +269,24 @@ export async function validateNavigationUrl(url: string): Promise<string> {
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.`
);
}

View File

@ -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);
});