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); });