From 38ef581e7afcf0eb0cc9b5c987154ad5e7cfd49e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 4 Jun 2026 12:23:43 +0200 Subject: [PATCH] test(security): inventory guard for the SSRF requirePublicHost pins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Source-grep guard asserting the openai/anthropic/geo outbound sites keep the unconditional DNS-rebinding pin and the local AI client keeps its conditional LAN escape hatch — so a future edit can't silently re-open the SSRF surface. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/__tests__/ssrf-pin-inventory.test.ts | 46 ++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/lib/__tests__/ssrf-pin-inventory.test.ts diff --git a/src/lib/__tests__/ssrf-pin-inventory.test.ts b/src/lib/__tests__/ssrf-pin-inventory.test.ts new file mode 100644 index 000000000..0b2dce059 --- /dev/null +++ b/src/lib/__tests__/ssrf-pin-inventory.test.ts @@ -0,0 +1,46 @@ +/** + * v1.11.2 — SSRF DNS-rebinding pin inventory. + * + * Source-text guard (same approach as the queue-registration + coach-gate + * inventory tests): the outbound `safeFetch` sites whose host is user- or + * operator-controlled MUST pass `requirePublicHost: true`, which wires both + * the input-time `isPublicUrl` check and the connect-time DNS-rebinding pin. + * A future edit that drops the pin (re-opening the SSRF surface) fails here + * instead of shipping silently. + * + * The LOCAL AI client is the deliberate exception: it stays CONDITIONAL + * (`requirePublicHost: !allowPrivate`) so an operator can opt into LAN hosts + * via `ALLOW_LOCAL_AI_PRIVATE_HOSTS`. We assert it keeps that conditional + * shape — never an unconditional `true` (which would break LAN local models) + * and never absent (which would re-open it for the default case). + */ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +const SRC = join(__dirname, ".."); +const read = (rel: string) => readFileSync(join(SRC, rel), "utf8"); + +describe("SSRF requirePublicHost pin inventory", () => { + it("openai-client pins the BYO base-URL outbound unconditionally", () => { + expect(read("ai/openai-client.ts")).toMatch(/requirePublicHost:\s*true/); + }); + + it("anthropic-client pins the BYO base-URL outbound unconditionally", () => { + expect(read("ai/anthropic-client.ts")).toMatch(/requirePublicHost:\s*true/); + }); + + it("geo lookup pins the operator IP_GEO_LOOKUP_URL outbound", () => { + expect(read("geo.ts")).toMatch(/requirePublicHost:\s*true/); + }); + + it("local AI client keeps the pin CONDITIONAL (LAN escape hatch)", () => { + const src = read("ai/local-client.ts"); + // Must carry the conditional form … + expect(src).toMatch(/requirePublicHost:\s*!?\s*allowPrivate/); + // … and must NOT be hardened to an unconditional true (that would break + // a deliberately-private LAN local model). + expect(src).not.toMatch(/requirePublicHost:\s*true/); + }); +});