diff --git a/src/infra/net/fetch-guard.ssrf.test.ts b/src/infra/net/fetch-guard.ssrf.test.ts index e686f46e0..4e6410c4b 100644 --- a/src/infra/net/fetch-guard.ssrf.test.ts +++ b/src/infra/net/fetch-guard.ssrf.test.ts @@ -15,6 +15,43 @@ function okResponse(body = "ok"): Response { describe("fetchWithSsrFGuard hardening", () => { type LookupFn = NonNullable[0]["lookupFn"]>; + + const createPublicLookup = (): LookupFn => + vi.fn(async () => [{ address: "93.184.216.34", family: 4 }]) as unknown as LookupFn; + + const getSecondRequestHeaders = (fetchImpl: ReturnType): Headers => { + const [, secondInit] = fetchImpl.mock.calls[1] as [string, RequestInit]; + return new Headers(secondInit.headers); + }; + + async function runProxyModeDispatcherTest(params: { + mode: (typeof GUARDED_FETCH_MODE)[keyof typeof GUARDED_FETCH_MODE]; + expectEnvProxy: boolean; + }): Promise { + vi.stubEnv("HTTP_PROXY", "http://127.0.0.1:7890"); + const lookupFn = createPublicLookup(); + const fetchImpl = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { + const requestInit = init as RequestInit & { dispatcher?: unknown }; + if (params.expectEnvProxy) { + expect(requestInit.dispatcher).toBeInstanceOf(EnvHttpProxyAgent); + } else { + expect(requestInit.dispatcher).toBeDefined(); + expect(requestInit.dispatcher).not.toBeInstanceOf(EnvHttpProxyAgent); + } + return okResponse(); + }); + + const result = await fetchWithSsrFGuard({ + url: "https://public.example/resource", + fetchImpl, + lookupFn, + mode: params.mode, + }); + + expect(fetchImpl).toHaveBeenCalledTimes(1); + await result.release(); + } + afterEach(() => { vi.unstubAllEnvs(); }); @@ -60,9 +97,7 @@ describe("fetchWithSsrFGuard hardening", () => { }); 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 lookupFn = createPublicLookup(); const fetchImpl = vi.fn().mockResolvedValueOnce(redirectResponse("http://127.0.0.1:6379/")); await expect( @@ -88,9 +123,7 @@ describe("fetchWithSsrFGuard hardening", () => { }); it("allows wildcard allowlisted hosts", async () => { - const lookupFn = vi.fn(async () => [ - { address: "93.184.216.34", family: 4 }, - ]) as unknown as LookupFn; + const lookupFn = createPublicLookup(); const fetchImpl = vi.fn(async () => new Response("ok", { status: 200 })); const result = await fetchWithSsrFGuard({ url: "https://img.assets.example.com/pic.png", @@ -105,9 +138,7 @@ describe("fetchWithSsrFGuard hardening", () => { }); it("strips sensitive headers when redirect crosses origins", async () => { - const lookupFn = vi.fn(async () => [ - { address: "93.184.216.34", family: 4 }, - ]) as unknown as LookupFn; + const lookupFn = createPublicLookup(); const fetchImpl = vi .fn() .mockResolvedValueOnce(redirectResponse("https://cdn.example.com/asset")) @@ -128,8 +159,7 @@ describe("fetchWithSsrFGuard hardening", () => { }, }); - const [, secondInit] = fetchImpl.mock.calls[1] as [string, RequestInit]; - const headers = new Headers(secondInit.headers); + const headers = getSecondRequestHeaders(fetchImpl); expect(headers.get("authorization")).toBeNull(); expect(headers.get("proxy-authorization")).toBeNull(); expect(headers.get("cookie")).toBeNull(); @@ -139,9 +169,7 @@ describe("fetchWithSsrFGuard hardening", () => { }); it("keeps headers when redirect stays on same origin", async () => { - const lookupFn = vi.fn(async () => [ - { address: "93.184.216.34", family: 4 }, - ]) as unknown as LookupFn; + const lookupFn = createPublicLookup(); const fetchImpl = vi .fn() .mockResolvedValueOnce(redirectResponse("/next")) @@ -158,54 +186,22 @@ describe("fetchWithSsrFGuard hardening", () => { }, }); - const [, secondInit] = fetchImpl.mock.calls[1] as [string, RequestInit]; - const headers = new Headers(secondInit.headers); + const headers = getSecondRequestHeaders(fetchImpl); expect(headers.get("authorization")).toBe("Bearer secret"); await result.release(); }); it("ignores env proxy by default to preserve DNS-pinned destination binding", async () => { - vi.stubEnv("HTTP_PROXY", "http://127.0.0.1:7890"); - const lookupFn = vi.fn(async () => [ - { address: "93.184.216.34", family: 4 }, - ]) as unknown as LookupFn; - const fetchImpl = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { - const requestInit = init as RequestInit & { dispatcher?: unknown }; - expect(requestInit.dispatcher).toBeDefined(); - expect(requestInit.dispatcher).not.toBeInstanceOf(EnvHttpProxyAgent); - return okResponse(); - }); - - const result = await fetchWithSsrFGuard({ - url: "https://public.example/resource", - fetchImpl, - lookupFn, + await runProxyModeDispatcherTest({ mode: GUARDED_FETCH_MODE.STRICT, + expectEnvProxy: false, }); - - expect(fetchImpl).toHaveBeenCalledTimes(1); - await result.release(); }); it("uses env proxy only when dangerous proxy bypass is explicitly enabled", async () => { - vi.stubEnv("HTTP_PROXY", "http://127.0.0.1:7890"); - const lookupFn = vi.fn(async () => [ - { address: "93.184.216.34", family: 4 }, - ]) as unknown as LookupFn; - const fetchImpl = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { - const requestInit = init as RequestInit & { dispatcher?: unknown }; - expect(requestInit.dispatcher).toBeInstanceOf(EnvHttpProxyAgent); - return okResponse(); - }); - - const result = await fetchWithSsrFGuard({ - url: "https://public.example/resource", - fetchImpl, - lookupFn, + await runProxyModeDispatcherTest({ mode: GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY, + expectEnvProxy: true, }); - - expect(fetchImpl).toHaveBeenCalledTimes(1); - await result.release(); }); });