fix(infra/net): pass pinned DNS lookup to EnvHttpProxyAgent in trusted_env_proxy mode (CWE-918)#45248
fix(infra/net): pass pinned DNS lookup to EnvHttpProxyAgent in trusted_env_proxy mode (CWE-918)#45248kvenux wants to merge 2 commits intoopenclaw:mainfrom
Conversation
…d_env_proxy mode (CWE-918) In trusted_env_proxy mode, fetchWithSsrFGuard created a bare EnvHttpProxyAgent() discarding the pinned DNS lookup from resolvePinnedHostnameWithPolicy(). This allowed DNS rebinding attacks where a hostname resolves to a public IP at validation time but rebinds to a private/internal IP when the proxy agent performs its own DNS resolution. Fix by passing pinned.lookup to the EnvHttpProxyAgent connect options. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Greptile SummaryThis PR fixes a DNS rebinding / SSRF bypass (CWE-918) in
Confidence Score: 4/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/infra/net/fetch-guard.proxy.security.test.ts
Line: 52-69
Comment:
**Test does not actually validate DNS rebinding protection**
This test does not simulate a DNS rebinding attack — it simulates a hostname that *always* resolves to a private IP (`127.0.0.1`). Because `resolvePinnedHostnameWithPolicy` performs the SSRF check in Phase 2 (`assertAllowedResolvedAddressesOrThrow`) immediately after the DNS lookup, the call throws a `SsrFBlockedError` **before** the `EnvHttpProxyAgent` is ever created. This means the test would pass even if you reverted the entire fix to `new EnvHttpProxyAgent()`.
A genuine rebinding test needs the lookup to return different results at different call times — a public IP on the first call (letting validation pass) and a private IP on any subsequent call (simulating the rebind). The fix's protection is that `pinned.lookup` pins the pre-validated addresses, preventing `EnvHttpProxyAgent` from performing a fresh DNS resolution that would return the rebound IP. The current test never reaches that code path.
Example of a lookup that could exercise the actual fix:
```typescript
let callCount = 0;
const rebindLookup = vi.fn(async () => {
callCount += 1;
// First call: public IP (passes validation), second call: private IP (the rebind)
return callCount === 1
? [{ address: "93.184.216.34", family: 4 }]
: [{ address: "127.0.0.1", family: 4 }];
}) as unknown as LookupFn;
```
Note that verifying this scenario in a pure unit test is difficult because `fetchImpl` is mocked and won't trigger a second DNS resolution through undici; an integration-level test would be needed to fully exercise the `connect.lookup` path inside `EnvHttpProxyAgent`.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/infra/net/fetch-guard.proxy.security.test.ts
Line: 15-36
Comment:
**Test assertion does not verify the pinned lookup is wired in**
The assertion `expect(capturedDispatcher).toBeInstanceOf(EnvHttpProxyAgent)` was already true **before the fix**, since `new EnvHttpProxyAgent()` (without options) is still an `EnvHttpProxyAgent`. This test therefore does not distinguish the before-fix from the after-fix behaviour, and would pass if the fix were reverted.
To verify the actual change, the test should inspect the dispatcher's internal `options` or, more robustly, spy on the `EnvHttpProxyAgent` constructor and assert it was called with `connect.lookup` set:
```typescript
const EnvHttpProxyAgentSpy = vi.spyOn(
await import("undici"),
"EnvHttpProxyAgent"
);
// ... run fetchWithSsrFGuard ...
expect(EnvHttpProxyAgentSpy).toHaveBeenCalledWith(
expect.objectContaining({
connect: expect.objectContaining({ lookup: expect.any(Function) }),
})
);
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 9b2d6c4 |
| it("should block DNS rebinding via trusted_env_proxy", async () => { | ||
| vi.stubEnv("HTTP_PROXY", "http://127.0.0.1:7890"); | ||
| // Simulate DNS rebinding: hostname resolves to private IP | ||
| const rebindLookup = vi.fn(async () => [ | ||
| { address: "127.0.0.1", family: 4 }, | ||
| ]) as unknown as LookupFn; | ||
| const fetchImpl = vi.fn(); | ||
|
|
||
| await expect( | ||
| fetchWithSsrFGuard({ | ||
| url: "https://rebind.attacker.com/steal", | ||
| fetchImpl, | ||
| lookupFn: rebindLookup, | ||
| mode: GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY, | ||
| }), | ||
| ).rejects.toThrow(/private|internal|blocked/i); | ||
| expect(fetchImpl).not.toHaveBeenCalled(); | ||
| }); |
There was a problem hiding this comment.
Test does not actually validate DNS rebinding protection
This test does not simulate a DNS rebinding attack — it simulates a hostname that always resolves to a private IP (127.0.0.1). Because resolvePinnedHostnameWithPolicy performs the SSRF check in Phase 2 (assertAllowedResolvedAddressesOrThrow) immediately after the DNS lookup, the call throws a SsrFBlockedError before the EnvHttpProxyAgent is ever created. This means the test would pass even if you reverted the entire fix to new EnvHttpProxyAgent().
A genuine rebinding test needs the lookup to return different results at different call times — a public IP on the first call (letting validation pass) and a private IP on any subsequent call (simulating the rebind). The fix's protection is that pinned.lookup pins the pre-validated addresses, preventing EnvHttpProxyAgent from performing a fresh DNS resolution that would return the rebound IP. The current test never reaches that code path.
Example of a lookup that could exercise the actual fix:
let callCount = 0;
const rebindLookup = vi.fn(async () => {
callCount += 1;
// First call: public IP (passes validation), second call: private IP (the rebind)
return callCount === 1
? [{ address: "93.184.216.34", family: 4 }]
: [{ address: "127.0.0.1", family: 4 }];
}) as unknown as LookupFn;Note that verifying this scenario in a pure unit test is difficult because fetchImpl is mocked and won't trigger a second DNS resolution through undici; an integration-level test would be needed to fully exercise the connect.lookup path inside EnvHttpProxyAgent.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/net/fetch-guard.proxy.security.test.ts
Line: 52-69
Comment:
**Test does not actually validate DNS rebinding protection**
This test does not simulate a DNS rebinding attack — it simulates a hostname that *always* resolves to a private IP (`127.0.0.1`). Because `resolvePinnedHostnameWithPolicy` performs the SSRF check in Phase 2 (`assertAllowedResolvedAddressesOrThrow`) immediately after the DNS lookup, the call throws a `SsrFBlockedError` **before** the `EnvHttpProxyAgent` is ever created. This means the test would pass even if you reverted the entire fix to `new EnvHttpProxyAgent()`.
A genuine rebinding test needs the lookup to return different results at different call times — a public IP on the first call (letting validation pass) and a private IP on any subsequent call (simulating the rebind). The fix's protection is that `pinned.lookup` pins the pre-validated addresses, preventing `EnvHttpProxyAgent` from performing a fresh DNS resolution that would return the rebound IP. The current test never reaches that code path.
Example of a lookup that could exercise the actual fix:
```typescript
let callCount = 0;
const rebindLookup = vi.fn(async () => {
callCount += 1;
// First call: public IP (passes validation), second call: private IP (the rebind)
return callCount === 1
? [{ address: "93.184.216.34", family: 4 }]
: [{ address: "127.0.0.1", family: 4 }];
}) as unknown as LookupFn;
```
Note that verifying this scenario in a pure unit test is difficult because `fetchImpl` is mocked and won't trigger a second DNS resolution through undici; an integration-level test would be needed to fully exercise the `connect.lookup` path inside `EnvHttpProxyAgent`.
How can I resolve this? If you propose a fix, please make it concise.| it("should pass pinned lookup to EnvHttpProxyAgent in trusted_env_proxy mode", async () => { | ||
| vi.stubEnv("HTTP_PROXY", "http://127.0.0.1:7890"); | ||
| const lookupFn = createPublicLookup(); | ||
| let capturedDispatcher: unknown = null; | ||
|
|
||
| const fetchImpl = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { | ||
| const requestInit = init as RequestInit & { dispatcher?: unknown }; | ||
| capturedDispatcher = requestInit.dispatcher; | ||
| return new Response("ok", { status: 200 }); | ||
| }); | ||
|
|
||
| const result = await fetchWithSsrFGuard({ | ||
| url: "https://public.example/resource", | ||
| fetchImpl, | ||
| lookupFn, | ||
| mode: GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY, | ||
| }); | ||
|
|
||
| expect(fetchImpl).toHaveBeenCalledTimes(1); | ||
| expect(capturedDispatcher).toBeInstanceOf(EnvHttpProxyAgent); | ||
| await result.release(); | ||
| }); |
There was a problem hiding this comment.
Test assertion does not verify the pinned lookup is wired in
The assertion expect(capturedDispatcher).toBeInstanceOf(EnvHttpProxyAgent) was already true before the fix, since new EnvHttpProxyAgent() (without options) is still an EnvHttpProxyAgent. This test therefore does not distinguish the before-fix from the after-fix behaviour, and would pass if the fix were reverted.
To verify the actual change, the test should inspect the dispatcher's internal options or, more robustly, spy on the EnvHttpProxyAgent constructor and assert it was called with connect.lookup set:
const EnvHttpProxyAgentSpy = vi.spyOn(
await import("undici"),
"EnvHttpProxyAgent"
);
// ... run fetchWithSsrFGuard ...
expect(EnvHttpProxyAgentSpy).toHaveBeenCalledWith(
expect.objectContaining({
connect: expect.objectContaining({ lookup: expect.any(Function) }),
})
);Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/net/fetch-guard.proxy.security.test.ts
Line: 15-36
Comment:
**Test assertion does not verify the pinned lookup is wired in**
The assertion `expect(capturedDispatcher).toBeInstanceOf(EnvHttpProxyAgent)` was already true **before the fix**, since `new EnvHttpProxyAgent()` (without options) is still an `EnvHttpProxyAgent`. This test therefore does not distinguish the before-fix from the after-fix behaviour, and would pass if the fix were reverted.
To verify the actual change, the test should inspect the dispatcher's internal `options` or, more robustly, spy on the `EnvHttpProxyAgent` constructor and assert it was called with `connect.lookup` set:
```typescript
const EnvHttpProxyAgentSpy = vi.spyOn(
await import("undici"),
"EnvHttpProxyAgent"
);
// ... run fetchWithSsrFGuard ...
expect(EnvHttpProxyAgentSpy).toHaveBeenCalledWith(
expect.objectContaining({
connect: expect.objectContaining({ lookup: expect.any(Function) }),
})
);
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9b2d6c4b94
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| dispatcher = new EnvHttpProxyAgent({ | ||
| connect: { lookup: pinned.lookup }, |
There was a problem hiding this comment.
Don’t treat EnvHttpProxyAgent connect.lookup as proxy-path pinning
Using new EnvHttpProxyAgent({ connect: { lookup: pinned.lookup } }) here does not pin DNS for requests that actually go through the proxy, so the rebinding gap remains in trusted_env_proxy mode for proxied targets. The repository already documents and tests that EnvHttpProxyAgent applies connect to the no-proxy agent path, while proxied HTTPS transport uses proxy-specific settings instead (src/telegram/fetch.ts:260-262, src/telegram/fetch.env-proxy-runtime.test.ts:29-56), so this change only protects NO_PROXY/direct hops, not the proxied flow described in the commit message.
Useful? React with 👍 / 👎.
| }); | ||
|
|
||
| expect(fetchImpl).toHaveBeenCalledTimes(1); | ||
| expect(capturedDispatcher).toBeInstanceOf(EnvHttpProxyAgent); |
There was a problem hiding this comment.
Assert the dispatcher options, not just its class
This check only verifies that a dispatcher is an EnvHttpProxyAgent, so the test would still pass if the pinned lookup were accidentally removed and the constructor reverted to new EnvHttpProxyAgent(). Because this test is meant to guard a security fix, it needs to verify the lookup wiring itself (for example by inspecting the agent options/internal symbols) to catch regressions in future refactors.
Useful? React with 👍 / 👎.
Previous test only checked instanceof which was true before the fix. Now spy on the constructor and assert connect.lookup is a function. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8410518955
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // DNS rebinding attacks. A bare EnvHttpProxyAgent() would perform | ||
| // its own DNS resolution, discarding the validated IP addresses. | ||
| dispatcher = new EnvHttpProxyAgent({ | ||
| connect: { lookup: pinned.lookup }, |
There was a problem hiding this comment.
Do not rely on connect.lookup for proxied requests
In trusted_env_proxy mode this sets EnvHttpProxyAgent({ connect: { lookup } }), but for requests that actually go through HTTP(S)_PROXY (i.e., not in NO_PROXY) Undici does not use connect for the proxied HTTPS transport path, so the pinned resolver is bypassed and DNS can still be rebound after validation. This means the commit’s security fix only affects direct/no-proxy hops, not the proxied flow it claims to protect.
Useful? React with 👍 / 👎.
| vi.mock("undici", async (importOriginal) => { | ||
| const actual = (await importOriginal()) as Record<string, unknown>; | ||
| return { ...actual, EnvHttpProxyAgent: envHttpProxyAgentCtor }; |
There was a problem hiding this comment.
Exercise the real proxy agent in the security regression test
This test replaces EnvHttpProxyAgent with a mock constructor and only asserts that a function is passed in options, so it cannot detect whether real proxied traffic actually honors that lookup. As written, the test still passes even if the runtime proxy path ignores connect.lookup, which makes the security regression check a false positive for the vulnerability this commit targets.
Useful? React with 👍 / 👎.
…s configured When HTTP proxy environment variables (HTTPS_PROXY, etc.) are set, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy entirely. This fails in environments where direct outbound connections are blocked: - OpenShell/NemoClaw sandboxes (network namespace blocks direct TCP) - Docker containers with network isolation - Corporate networks requiring proxy for all HTTPS traffic The SSRF hostname validation (`resolvePinnedHostnameWithPolicy`) still runs before the fetch — this change only affects how the connection is established, not what's allowed. Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248 (improved TRUSTED_ENV_PROXY mode, but STRICT mode still broken)
…ng DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248
…ng DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248
…ng DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248
…ng DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248
…ng DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248
…ng DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248
…ng DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248
…ng DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248
…ng DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248
…ng DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248
…ng DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248
* fix(infra/net): route through env proxy in STRICT mode while preserving DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes #47598, #49948, #32947, #46306 Related: #45248 * test(infra): stabilize fetch guard proxy assertions * fix: respect hostname-scoped proxy bypass (#50650) (thanks @kkav004) --------- Co-authored-by: Kiryl Kavalenka <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
…kav004) * fix(infra/net): route through env proxy in STRICT mode while preserving DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248 * test(infra): stabilize fetch guard proxy assertions * fix: respect hostname-scoped proxy bypass (openclaw#50650) (thanks @kkav004) --------- Co-authored-by: Kiryl Kavalenka <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
…kav004) * fix(infra/net): route through env proxy in STRICT mode while preserving DNS pinning When HTTP_PROXY/HTTPS_PROXY env vars are configured, the SSRF guard's pinned dispatcher connects directly to the DNS-resolved IP, bypassing the proxy. This fails in environments where direct outbound connections are blocked (OpenShell sandboxes, Docker containers, corporate networks). Use `createPinnedDispatcher` with `mode: "env-proxy"` when `hasEnvHttpProxyConfigured()` returns true. This preserves DNS-pinning (the resolved IP is threaded into the connect option via `EnvHttpProxyAgent`) while routing through the proxy. - Uses `hasEnvHttpProxyConfigured()` (not `hasProxyEnvConfigured()`) to avoid the ALL_PROXY edge case where EnvHttpProxyAgent ignores ALL_PROXY - Preserves STRICT mode's anti-DNS-rebinding guarantee - TRUSTED_ENV_PROXY remains the explicit opt-in for unpinned proxy routing - No change when proxy env vars are not set Fixes openclaw#47598, openclaw#49948, openclaw#32947, openclaw#46306 Related: openclaw#45248 * test(infra): stabilize fetch guard proxy assertions * fix: respect hostname-scoped proxy bypass (openclaw#50650) (thanks @kkav004) --------- Co-authored-by: Kiryl Kavalenka <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
Summary
Fix DNS pinning bypass in trusted_env_proxy mode where EnvHttpProxyAgent was created without the pinned lookup, allowing DNS rebinding attacks (CWE-918).
Vulnerability
Reproduction
Fix
Pass pinned.lookup to EnvHttpProxyAgent via connect options, ensuring the proxy agent uses the pre-validated IP addresses instead of performing its own DNS resolution:
Test Results
After fix (PASS)
Full regression: all infra/net tests pass
Checklist
AI Disclosure