openclaw/src/infra/net/fetch-guard.ssrf.test.ts
2026-02-19 15:24:47 +01:00

92 lines
2.8 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { fetchWithSsrFGuard } from "./fetch-guard.js";
function redirectResponse(location: string): Response {
return new Response(null, {
status: 302,
headers: { location },
});
}
describe("fetchWithSsrFGuard hardening", () => {
type LookupFn = NonNullable<Parameters<typeof fetchWithSsrFGuard>[0]["lookupFn"]>;
it("blocks private IP literal URLs before fetch", async () => {
const fetchImpl = vi.fn();
await expect(
fetchWithSsrFGuard({
url: "http://127.0.0.1:8080/internal",
fetchImpl,
}),
).rejects.toThrow(/private|internal|blocked/i);
expect(fetchImpl).not.toHaveBeenCalled();
});
it("blocks legacy loopback literal URLs before fetch", async () => {
const fetchImpl = vi.fn();
await expect(
fetchWithSsrFGuard({
url: "http://0177.0.0.1:8080/internal",
fetchImpl,
}),
).rejects.toThrow(/private|internal|blocked/i);
expect(fetchImpl).not.toHaveBeenCalled();
});
it("blocks unsupported packed-hex loopback literal URLs before fetch", async () => {
const fetchImpl = vi.fn();
await expect(
fetchWithSsrFGuard({
url: "http://0x7f000001/internal",
fetchImpl,
}),
).rejects.toThrow(/private|internal|blocked/i);
expect(fetchImpl).not.toHaveBeenCalled();
});
it("blocks redirect chains that hop to private hosts", async () => {
const lookupFn = vi.fn(async () => [
{ address: "93.184.216.34", family: 4 },
]) as unknown as LookupFn;
const fetchImpl = vi.fn().mockResolvedValueOnce(redirectResponse("http://127.0.0.1:6379/"));
await expect(
fetchWithSsrFGuard({
url: "https://public.example/start",
fetchImpl,
lookupFn,
}),
).rejects.toThrow(/private|internal|blocked/i);
expect(fetchImpl).toHaveBeenCalledTimes(1);
});
it("enforces hostname allowlist policies", async () => {
const fetchImpl = vi.fn();
await expect(
fetchWithSsrFGuard({
url: "https://evil.example.org/file.txt",
fetchImpl,
policy: { hostnameAllowlist: ["cdn.example.com", "*.assets.example.com"] },
}),
).rejects.toThrow(/allowlist/i);
expect(fetchImpl).not.toHaveBeenCalled();
});
it("allows wildcard allowlisted hosts", async () => {
const lookupFn = vi.fn(async () => [
{ address: "93.184.216.34", family: 4 },
]) as unknown as LookupFn;
const fetchImpl = vi.fn(async () => new Response("ok", { status: 200 }));
const result = await fetchWithSsrFGuard({
url: "https://img.assets.example.com/pic.png",
fetchImpl,
lookupFn,
policy: { hostnameAllowlist: ["*.assets.example.com"] },
});
expect(result.response.status).toBe(200);
expect(fetchImpl).toHaveBeenCalledTimes(1);
await result.release();
});
});