92 lines
2.8 KiB
TypeScript
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();
|
|
});
|
|
});
|