fix(ssrf): pass pinned DNS lookup to ProxyAgent via requestTls#46756
fix(ssrf): pass pinned DNS lookup to ProxyAgent via requestTls#46756yangyitao100 wants to merge 1 commit into
Conversation
Greptile SummaryThis PR attempts to close a SSRF gap in explicit-proxy mode by passing Key concerns:
Confidence Score: 2/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/infra/net/ssrf.ts
Line: 376-384
Comment:
**`requestTls.lookup` may not enforce DNS pinning in proxy mode**
The fix places `lookup` inside `requestTls`, but `requestTls` configures TLS settings for connections to the origin server — it is not where DNS resolution is controlled.
In standard HTTP CONNECT proxy flow:
1. Client connects TCP/TLS to the proxy (using `proxyTls`)
2. Client sends `CONNECT api.telegram.org:443`
3. **The proxy** resolves `api.telegram.org` via its own DNS and opens a TCP connection to the origin
4. Client receives `200 Connection Established` and performs TLS over the tunnel socket (`requestTls`)
At step 4, there is no new TCP connection from the client, so `net.connect()` (which honours the `lookup` option) is never called. The pinned `lookup` function is therefore never invoked and the DNS rebinding protection does not apply.
Contrast this with the `Agent` / `EnvHttpProxyAgent` branches, which pass `lookup` inside `connect` — these do result in a fresh TCP connection from the client with custom DNS resolution.
The tests only mock the `ProxyAgent` constructor and verify which arguments are passed; they do not exercise the actual undici request dispatch path, so they cannot catch this discrepancy.
To properly guard against DNS rebinding in explicit-proxy mode the protection needs to be applied before the request is dispatched (e.g., validating that the request hostname matches the pre-resolved pinned hostname at dispatch time), rather than relying on a `lookup` override that is unlikely to be reached.
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/ssrf.dispatcher.test.ts
Line: 130-147
Comment:
**Tests only verify constructor arguments, not actual DNS pinning**
Both the updated test and the new test verify that `ProxyAgent` is instantiated with the expected options using a fully-mocked constructor. This does not exercise the actual undici `ProxyAgent` request dispatch path, so these tests would pass even if `requestTls.lookup` is never called at runtime.
Consider adding an integration-style test (or at minimum a unit test that stubs `net.connect` / the undici connector) that verifies the pinned `lookup` function is actually invoked when a request is dispatched through the `ProxyAgent`. Without this, the correctness of the DNS pinning behaviour in explicit-proxy mode is not validated.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 3730f48 |
| const requestTls = withPinnedLookup(pinned.lookup); | ||
| if (!policy.proxyTls) { | ||
| return new ProxyAgent(proxyUrl); | ||
| return new ProxyAgent({ uri: proxyUrl, requestTls }); | ||
| } | ||
| return new ProxyAgent({ | ||
| uri: proxyUrl, | ||
| requestTls, | ||
| proxyTls: { ...policy.proxyTls }, | ||
| }); |
There was a problem hiding this comment.
requestTls.lookup may not enforce DNS pinning in proxy mode
The fix places lookup inside requestTls, but requestTls configures TLS settings for connections to the origin server — it is not where DNS resolution is controlled.
In standard HTTP CONNECT proxy flow:
- Client connects TCP/TLS to the proxy (using
proxyTls) - Client sends
CONNECT api.telegram.org:443 - The proxy resolves
api.telegram.orgvia its own DNS and opens a TCP connection to the origin - Client receives
200 Connection Establishedand performs TLS over the tunnel socket (requestTls)
At step 4, there is no new TCP connection from the client, so net.connect() (which honours the lookup option) is never called. The pinned lookup function is therefore never invoked and the DNS rebinding protection does not apply.
Contrast this with the Agent / EnvHttpProxyAgent branches, which pass lookup inside connect — these do result in a fresh TCP connection from the client with custom DNS resolution.
The tests only mock the ProxyAgent constructor and verify which arguments are passed; they do not exercise the actual undici request dispatch path, so they cannot catch this discrepancy.
To properly guard against DNS rebinding in explicit-proxy mode the protection needs to be applied before the request is dispatched (e.g., validating that the request hostname matches the pre-resolved pinned hostname at dispatch time), rather than relying on a lookup override that is unlikely to be reached.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/net/ssrf.ts
Line: 376-384
Comment:
**`requestTls.lookup` may not enforce DNS pinning in proxy mode**
The fix places `lookup` inside `requestTls`, but `requestTls` configures TLS settings for connections to the origin server — it is not where DNS resolution is controlled.
In standard HTTP CONNECT proxy flow:
1. Client connects TCP/TLS to the proxy (using `proxyTls`)
2. Client sends `CONNECT api.telegram.org:443`
3. **The proxy** resolves `api.telegram.org` via its own DNS and opens a TCP connection to the origin
4. Client receives `200 Connection Established` and performs TLS over the tunnel socket (`requestTls`)
At step 4, there is no new TCP connection from the client, so `net.connect()` (which honours the `lookup` option) is never called. The pinned `lookup` function is therefore never invoked and the DNS rebinding protection does not apply.
Contrast this with the `Agent` / `EnvHttpProxyAgent` branches, which pass `lookup` inside `connect` — these do result in a fresh TCP connection from the client with custom DNS resolution.
The tests only mock the `ProxyAgent` constructor and verify which arguments are passed; they do not exercise the actual undici request dispatch path, so they cannot catch this discrepancy.
To properly guard against DNS rebinding in explicit-proxy mode the protection needs to be applied before the request is dispatched (e.g., validating that the request hostname matches the pre-resolved pinned hostname at dispatch time), rather than relying on a `lookup` override that is unlikely to be reached.
How can I resolve this? If you propose a fix, please make it concise.| it("passes pinned lookup via requestTls when proxyTls is absent", () => { | ||
| const lookup = vi.fn() as unknown as PinnedHostname["lookup"]; | ||
| const pinned: PinnedHostname = { | ||
| hostname: "api.telegram.org", | ||
| addresses: ["149.154.167.220"], | ||
| lookup, | ||
| }; | ||
|
|
||
| createPinnedDispatcher(pinned, { | ||
| mode: "explicit-proxy", | ||
| proxyUrl: "http://127.0.0.1:7890", | ||
| }); | ||
|
|
||
| expect(proxyAgentCtor).toHaveBeenCalledWith({ | ||
| uri: "http://127.0.0.1:7890", | ||
| requestTls: { lookup }, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Tests only verify constructor arguments, not actual DNS pinning
Both the updated test and the new test verify that ProxyAgent is instantiated with the expected options using a fully-mocked constructor. This does not exercise the actual undici ProxyAgent request dispatch path, so these tests would pass even if requestTls.lookup is never called at runtime.
Consider adding an integration-style test (or at minimum a unit test that stubs net.connect / the undici connector) that verifies the pinned lookup function is actually invoked when a request is dispatched through the ProxyAgent. Without this, the correctness of the DNS pinning behaviour in explicit-proxy mode is not validated.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/net/ssrf.dispatcher.test.ts
Line: 130-147
Comment:
**Tests only verify constructor arguments, not actual DNS pinning**
Both the updated test and the new test verify that `ProxyAgent` is instantiated with the expected options using a fully-mocked constructor. This does not exercise the actual undici `ProxyAgent` request dispatch path, so these tests would pass even if `requestTls.lookup` is never called at runtime.
Consider adding an integration-style test (or at minimum a unit test that stubs `net.connect` / the undici connector) that verifies the pinned `lookup` function is actually invoked when a request is dispatched through the `ProxyAgent`. Without this, the correctness of the DNS pinning behaviour in explicit-proxy mode is not validated.
How can I resolve this? If you propose a fix, please make it concise.createPinnedDispatcher() passed the SSRF-safe pinned lookup to Agent and EnvHttpProxyAgent but skipped it for ProxyAgent (explicit-proxy mode). This meant DNS resolution for origin servers behind an explicit proxy bypassed the pinned hostname check, potentially allowing DNS rebinding. Pass the pinned lookup in requestTls for ProxyAgent so all dispatcher modes enforce DNS pinning consistently. Closes openclaw#46685
3730f48 to
aa54fa9
Compare
|
Closing: too many versions behind, will re-evaluate on latest codebase. |
Summary
requestTlsfor SSRF protectionProblem
createPinnedDispatcher()correctly passes the SSRF-safe pinned hostname lookup toAgent(direct mode) andEnvHttpProxyAgent(env-proxy mode) via theirconnectoptions. However, forProxyAgent(explicit-proxy mode), the pinned lookup was never passed. This meant DNS resolution for origin servers behind an explicit proxy bypassed the pinned hostname check, potentially allowing DNS rebinding attacks.Root Cause
In
src/infra/net/ssrf.ts, the explicit-proxy branch (lines 372-379) constructedProxyAgenteither with a plain URL string or with{ uri, proxyTls }— neither of which included therequestTlsoption needed to carry the pinned lookup function.Fix
requestTls: { lookup: pinned.lookup }when constructingProxyAgentrequestTlsin the callrequestTlsis passed even whenproxyTlsis absentTest Plan
proxyTlsnow expectsrequestTls: { lookup }proxyTlsalso getsrequestTls: { lookup }Closes #46685
🤖 Generated with Claude Code