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

@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
- Gateway/Security: require secure context and paired-device checks for Control UI auth even when `gateway.controlUi.allowInsecureAuth` is set, and align audit messaging with the hardened behavior. (#20684) thanks @coygeek.
- Docker/Security: run E2E and install-sh test images as non-root by adding appuser directives. Thanks @thewilloftheshadow.
- Skills/Security: sanitize skill env overrides to block unsafe runtime injection variables and only allow sensitive keys when declared in skill metadata, with warnings for suspicious values. Thanks @thewilloftheshadow.
- Security/Browser: block non-network browser navigation protocols (including `file:`, `data:`, and `javascript:`) while preserving `about:blank`, preventing local file reads via browser tool navigation. This ships in the next npm release. Thanks @q1uf3ng for reporting.
- Security/Gateway/Hooks: block `__proto__`, `constructor`, and `prototype` traversal in webhook template path resolution to prevent prototype-chain payload data leakage in `messageTemplate` rendering. (#22213) Thanks @SleuthCo.
- Security/OpenClawKit/UI: prevent injected inbound user context metadata blocks from leaking into chat history in TUI, webchat, and macOS surfaces by stripping all untrusted metadata prefixes at display boundaries. (#22142) Thanks @Mellowambience, @vincentkoc.
- Security/OpenClawKit/UI: strip inbound metadata blocks from user messages in TUI rendering while preserving user-authored content. (#22345) Thanks @kansodata, @vincentkoc.

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, {