105 lines
3.8 KiB
JavaScript
105 lines
3.8 KiB
JavaScript
// 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,<script>alert(1)</script>",
|
|
"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);
|
|
}
|
|
});
|
|
});
|