Files
pi-agent-integrated/pi-web/lib/request-security.test.mjs
T

169 lines
5.9 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
async function loadSubject() {
return import("./request-security.ts");
}
test("allows same-origin and non-browser API requests", async () => {
const { isApiRequestAllowed } = await loadSubject();
assert.equal(isApiRequestAllowed(new Request("http://localhost:30141/api/test", {
method: "POST",
headers: {
host: "localhost:30141",
origin: "http://localhost:30141",
"sec-fetch-site": "same-origin",
},
})), true);
assert.equal(isApiRequestAllowed(new Request("http://localhost:30141/api/test", {
method: "POST",
headers: { host: "localhost:30141" },
})), true);
});
test("allows LAN same-origin requests when Next.js uses an internal localhost URL", async () => {
const { isApiRequestAllowed } = await loadSubject();
const request = new Request("http://localhost:30141/api/test", {
method: "POST",
headers: {
host: "192.168.32.7:30141",
origin: "http://192.168.32.7:30141",
"sec-fetch-site": "same-origin",
},
});
assert.equal(isApiRequestAllowed(request), true);
});
test("allows IPv6 and an explicitly configured hostname", async () => {
const { isApiRequestAllowed } = await loadSubject();
const ipv6 = new Request("http://localhost:30141/api/test", {
method: "POST",
headers: {
host: "[::1]:30141",
origin: "http://[::1]:30141",
"sec-fetch-site": "same-origin",
},
});
const configured = new Request("http://localhost:30141/api/test", {
method: "POST",
headers: {
host: "pi-web.internal:30141",
origin: "http://pi-web.internal:30141",
"sec-fetch-site": "same-origin",
},
});
assert.equal(isApiRequestAllowed(ipv6), true);
assert.equal(isApiRequestAllowed(configured, ["pi-web.internal"]), true);
});
test("rejects cross-origin browser API requests", async () => {
const { isApiRequestAllowed, shouldCheckApiRequestOrigin } = await loadSubject();
const post = new Request("http://localhost:30141/api/test", {
method: "POST",
headers: {
host: "localhost:30141",
origin: "https://attacker.example",
"sec-fetch-site": "cross-site",
},
});
const crossSiteGet = new Request("http://localhost:30141/api/sessions", {
headers: { host: "localhost:30141", "sec-fetch-site": "cross-site" },
});
assert.equal(shouldCheckApiRequestOrigin(post), true);
assert.equal(isApiRequestAllowed(post), false);
assert.equal(shouldCheckApiRequestOrigin(crossSiteGet), true);
assert.equal(isApiRequestAllowed(crossSiteGet), false);
});
test("allows only user-initiated session export document navigations from a PWA", async () => {
const { isApiRequestAllowed } = await loadSubject();
const navigationHeaders = {
host: "127.0.0.1:30141",
"sec-fetch-site": "cross-site",
"sec-fetch-mode": "navigate",
"sec-fetch-dest": "document",
"sec-fetch-user": "?1",
};
assert.equal(isApiRequestAllowed(new Request(
"http://127.0.0.1:30141/api/sessions/session-id/export?inline=1",
{ headers: navigationHeaders },
)), true);
assert.equal(isApiRequestAllowed(new Request(
"http://127.0.0.1:30141/api/sessions",
{ headers: navigationHeaders },
)), false);
assert.equal(isApiRequestAllowed(new Request(
"http://127.0.0.1:30141/api/sessions/session-id/export?inline=1",
{ headers: { ...navigationHeaders, "sec-fetch-dest": "empty" } },
)), false);
assert.equal(isApiRequestAllowed(new Request(
"http://127.0.0.1:30141/api/sessions/session-id/export?inline=1",
{
headers: {
...navigationHeaders,
"sec-fetch-user": "",
},
},
)), false);
assert.equal(isApiRequestAllowed(new Request(
"http://127.0.0.1:30141/api/sessions/session-id/export?inline=1",
{ method: "POST", headers: navigationHeaders },
)), false);
assert.equal(isApiRequestAllowed(new Request(
"http://127.0.0.1:30141/api/sessions/session-id/export?inline=1",
{ headers: { ...navigationHeaders, host: "attacker.example:30141" } },
)), false);
});
test("rejects an origin that does not match the external request host", async () => {
const { isApiRequestAllowed } = await loadSubject();
const request = new Request("http://localhost:30141/api/test", {
method: "POST",
headers: {
host: "192.168.32.7:30141",
origin: "http://attacker.example",
"sec-fetch-site": "same-site",
},
});
assert.equal(isApiRequestAllowed(request), false);
});
test("rejects DNS rebinding even when browser headers say same-origin", async () => {
const { isApiRequestAllowed } = await loadSubject();
const request = new Request("http://localhost:30141/api/skills/install", {
method: "POST",
headers: {
host: "attacker.example:30141",
origin: "http://attacker.example:30141",
"sec-fetch-site": "same-origin",
"content-type": "application/json",
},
});
assert.equal(isApiRequestAllowed(request), false);
});
test("rejects missing, malformed, and unconfigured Host headers", async () => {
const { isApiRequestAllowed } = await loadSubject();
assert.equal(isApiRequestAllowed(new Request("http://localhost:30141/api/test")), false);
assert.equal(isApiRequestAllowed(new Request("http://localhost:30141/api/test", {
headers: { host: "localhost@attacker.example:30141" },
})), false);
assert.equal(isApiRequestAllowed(new Request("http://localhost:30141/api/test", {
headers: { host: "pi-web.internal:30141" },
})), false);
});
test("recognizes JSON request content types", async () => {
const { hasJsonContentType } = await loadSubject();
assert.equal(hasJsonContentType(new Request("http://localhost", {
headers: { "content-type": "application/json; charset=utf-8" },
})), true);
assert.equal(hasJsonContentType(new Request("http://localhost", {
headers: { "content-type": "application/problem+json" },
})), true);
assert.equal(hasJsonContentType(new Request("http://localhost", {
headers: { "content-type": "text/plain" },
})), false);
});