fix(browser): block non-network navigation schemes

This commit is contained in:
Peter Steinberger
2026-02-21 11:31:13 +01:00
parent c6ee14d60e
commit 220bd95eff
3 changed files with 41 additions and 2 deletions

View File

@@ -19,7 +19,7 @@ describe("browser navigation guard", () => {
).rejects.toBeInstanceOf(SsrFBlockedError);
});
it("allows non-network schemes", async () => {
it("allows about:blank", async () => {
await expect(
assertBrowserNavigationAllowed({
url: "about:blank",
@@ -27,6 +27,38 @@ describe("browser navigation guard", () => {
).resolves.toBeUndefined();
});
it("blocks file URLs", async () => {
await expect(
assertBrowserNavigationAllowed({
url: "file:///etc/passwd",
}),
).rejects.toBeInstanceOf(InvalidBrowserNavigationUrlError);
});
it("blocks data URLs", async () => {
await expect(
assertBrowserNavigationAllowed({
url: "data:text/html,<h1>owned</h1>",
}),
).rejects.toBeInstanceOf(InvalidBrowserNavigationUrlError);
});
it("blocks javascript URLs", async () => {
await expect(
assertBrowserNavigationAllowed({
url: "javascript:alert(1)",
}),
).rejects.toBeInstanceOf(InvalidBrowserNavigationUrlError);
});
it("blocks non-blank about URLs", async () => {
await expect(
assertBrowserNavigationAllowed({
url: "about:srcdoc",
}),
).rejects.toBeInstanceOf(InvalidBrowserNavigationUrlError);
});
it("allows blocked hostnames when explicitly allowed", async () => {
const lookupFn = createLookupFn("127.0.0.1");
await expect(

View File

@@ -5,6 +5,7 @@ import {
} from "../infra/net/ssrf.js";
const NETWORK_NAVIGATION_PROTOCOLS = new Set(["http:", "https:"]);
const SAFE_NON_NETWORK_URLS = new Set(["about:blank"]);
export class InvalidBrowserNavigationUrlError extends Error {
constructor(message: string) {
@@ -42,7 +43,12 @@ export async function assertBrowserNavigationAllowed(
}
if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
return;
if (SAFE_NON_NETWORK_URLS.has(parsed.href)) {
return;
}
throw new InvalidBrowserNavigationUrlError(
`Navigation blocked: unsupported protocol "${parsed.protocol}"`,
);
}
await resolvePinnedHostnameWithPolicy(parsed.hostname, {