diff --git a/CHANGELOG.md b/CHANGELOG.md index 84796d64f40..4e5096fec07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Slack: honor ambient HTTP(S) proxy settings for Socket Mode WebSocket connections, including NO_PROXY exclusions, so proxy-only deployments can connect without a monkey patch. (#62878) Thanks @mjamiv. +- Network/fetch guard: skip target DNS pinning when trusted env-proxy mode is active so proxy-only sandboxes can let the trusted proxy resolve outbound hosts. (#59007) Thanks @cluster2600. ## 2026.4.7-1 diff --git a/src/infra/net/fetch-guard.ssrf.test.ts b/src/infra/net/fetch-guard.ssrf.test.ts index 4ad9d2c4774..099c049ae43 100644 --- a/src/infra/net/fetch-guard.ssrf.test.ts +++ b/src/infra/net/fetch-guard.ssrf.test.ts @@ -81,6 +81,15 @@ async function expectRedirectFailure(params: { } describe("fetchWithSsrFGuard hardening", () => { + const PROXY_ENV_KEYS = [ + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", + ] as const; + type LookupFn = NonNullable[0]["lookupFn"]>; const CROSS_ORIGIN_REDIRECT_STRIPPED_HEADERS = [ "authorization", @@ -100,10 +109,17 @@ describe("fetchWithSsrFGuard hardening", () => { const createPublicLookup = (): LookupFn => vi.fn(async () => [{ address: "93.184.216.34", family: 4 }]) as unknown as LookupFn; + function clearProxyEnv(): void { + for (const key of PROXY_ENV_KEYS) { + vi.stubEnv(key, ""); + } + } + async function runProxyModeDispatcherTest(params: { mode: (typeof GUARDED_FETCH_MODE)[keyof typeof GUARDED_FETCH_MODE]; expectEnvProxy: boolean; }): Promise { + clearProxyEnv(); vi.stubEnv("HTTP_PROXY", "http://127.0.0.1:7890"); (globalThis as Record)[TEST_UNDICI_RUNTIME_DEPS_KEY] = { Agent: agentCtor, @@ -1032,4 +1048,26 @@ describe("fetchWithSsrFGuard hardening", () => { ).rejects.toThrow(/blocked/i); expect(fetchImpl).not.toHaveBeenCalled(); }); + + it("falls back to DNS pinning in trusted proxy mode when no proxy env var is configured", async () => { + clearProxyEnv(); + const lookupFn = createPublicLookup(); + const fetchImpl = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { + const requestInit = init as RequestInit & { dispatcher?: unknown }; + expect(requestInit.dispatcher).toBeDefined(); + expect(getDispatcherClassName(requestInit.dispatcher)).not.toBe("EnvHttpProxyAgent"); + return okResponse(); + }); + + const result = await fetchWithSsrFGuard({ + url: "https://public.example/resource", + fetchImpl, + lookupFn, + mode: GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY, + }); + + expect(fetchImpl).toHaveBeenCalledTimes(1); + expect(lookupFn).toHaveBeenCalledOnce(); + await result.release(); + }); }); diff --git a/src/infra/net/fetch-guard.ts b/src/infra/net/fetch-guard.ts index 72870de322f..27bb3e2160e 100644 --- a/src/infra/net/fetch-guard.ts +++ b/src/infra/net/fetch-guard.ts @@ -313,10 +313,6 @@ export async function fetchWithSsrFGuard(params: GuardedFetchOptions): Promise