// Tests for the agent-navigation URL allowlist (src/browserUrlPolicy.js), // run with `node --test`. Pure function — no Electron needed. // // The security property under test: a model-issued `browser_navigate` must not // be able to reach file://, cloud-metadata / loopback / private-range hosts, // or any non-http(s) scheme (else it screenshots the bytes back out = SSRF + // local-file read + exfil). Normal public https must still be allowed. const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); const { isAgentNavigationAllowed } = require("../src/browserUrlPolicy"); describe("browserUrlPolicy — agent navigation allowlist", () => { it("allows ordinary public http(s) URLs", () => { for (const url of [ "https://example.com/", "http://example.com/path?q=1", "https://sub.domain.example.org/a/b", "https://example.com:8443/x", // non-loopback host with a port is fine ]) { assert.equal(isAgentNavigationAllowed(url).ok, true, `expected allow: ${url}`); } }); it("rejects non-http(s) schemes (file/chrome/devtools/data/blob/javascript/about)", () => { for (const url of [ "file:///home/user/.ssh/id_rsa", "file:///etc/passwd", "chrome://settings", "devtools://devtools/bundled/inspector.html", "data:text/html,", "blob:https://example.com/uuid", "javascript:alert(document.cookie)", "about:blank", ]) { const v = isAgentNavigationAllowed(url); assert.equal(v.ok, false, `expected reject: ${url}`); assert.match(v.error, /navigation blocked/); } }); it("blocks cloud metadata + link-local (169.254.0.0/16)", () => { for (const url of [ "http://169.254.169.254/latest/meta-data/", "http://169.254.169.254/", "http://169.254.0.1/", "https://169.254.169.254/latest/meta-data/iam/security-credentials/", ]) { assert.equal(isAgentNavigationAllowed(url).ok, false, `expected reject: ${url}`); } }); it("blocks loopback: localhost, *.localhost, 127.0.0.0/8, ::1", () => { for (const url of [ "http://localhost/", "http://localhost:6767/health", "http://app.localhost/", "http://127.0.0.1/", "http://127.0.0.1:8080/admin", "http://127.5.6.7/", "http://[::1]/", ]) { assert.equal(isAgentNavigationAllowed(url).ok, false, `expected reject: ${url}`); } }); it("blocks RFC-1918 private ranges (10/8, 172.16/12, 192.168/16)", () => { for (const url of [ "http://10.0.0.1/", "http://10.255.255.255/", "http://172.16.0.1/", "http://172.20.10.5/", "http://172.31.255.255/", "http://192.168.0.1/", "http://192.168.1.100/internal", ]) { assert.equal(isAgentNavigationAllowed(url).ok, false, `expected reject: ${url}`); } }); it("allows the 172.x hosts that are NOT in the private /12 (172.15, 172.32)", () => { assert.equal(isAgentNavigationAllowed("http://172.15.0.1/").ok, true); assert.equal(isAgentNavigationAllowed("http://172.32.0.1/").ok, true); }); it("canonicalizes obfuscated IPv4 forms before checking (integer/hex/octal)", () => { // WHATWG URL normalizes these to 127.0.0.1 — the allowlist must still block. for (const url of [ "http://2130706433/", // 127.0.0.1 as a 32-bit int "http://0x7f000001/", // 127.0.0.1 as hex "http://0177.0.0.1/", // 127.0.0.1 with an octal first octet ]) { assert.equal(isAgentNavigationAllowed(url).ok, false, `expected reject: ${url}`); } }); it("rejects empty / malformed / non-string input without throwing", () => { for (const bad of ["", " ", "not a url", "://missing-scheme", null, undefined, 42]) { const v = isAgentNavigationAllowed(bad); assert.equal(v.ok, false); } }); });