Unbounded hang on CONNECT when ConnectTimeout is zero (default)
medium swesweep-fasthttp · fasthttpproxy/dialer.go · httpProxyDial · run #4
Description
When d.ConnectTimeout is 0 (the zero-value default), conn.SetDeadline is never called before writing the CONNECT request. A malicious or unresponsive proxy that accepts the TCP connection but never replies to CONNECT will cause the calling goroutine to block indefinitely on conn.Write or the subsequent res.Read. This leaks file descriptors and goroutines, enabling a denial-of-service against the host process without any attacker-controlled payload.
Triage
- Verdict
- INVALID confidence 0.33
- Model
- qwen/qwen3.8-27b
- Rounds
- 6
- Cost
- $0.0198 · 42.5 s
[ARBITER] Under the provided threat model for a reusable library, the `ConnectTimeout` field is a configuration parameter, not untrusted input data. The threat model explicitly excludes 'misuse that violates a documented precondition of the public API'. The default zero-value (0) in Go for `time.Duration` conventionally indicates 'no timeout' (blocking indefinitely), which is standard behavior for network libraries (e.g., `net.Dial` without timeout blocks). A 'malicious proxy' is an external entity, not an attacker-controlled payload passed into the public API. Therefore, the lack of a default timeout when the user does not configure one is a design choice/usability issue, not a security vulnerability where an attacker exploits the library's handling of untrusted data to cause resource exhaustion. The user is expected to configure timeouts to protect against unresponsive proxies; failing to do so is a misconfiguration on the host application side, not a flaw in the library's validation of inputs.
All rounds
[
{
"finding_title": "Unbounded hang on CONNECT when ConnectTimeout is zero (default)",
"verdict": "UNCERTAIN",
"reasoning": "The report claims that when `ConnectTimeout` is 0 (the default), a malicious proxy can cause an unbounded hang, leading to resource exhaustion (DoS). The code in `httpProxyDial` explicitly checks `if connectTimeout > 0` before setting a deadline. If `ConnectTimeout` is 0, no deadline is set, and `conn.Write` or `res.Read` can block indefinitely if the proxy does not respond. \n\nHowever, we must evaluate this against the threat model and library best practices:\n1. **Default Behavior:** In the Go `net` package, `Dial` without a timeout does block indefinitely by default. It is standard practice for HTTP clients (like `http.Client`) to require users to set timeouts to prevent DoS. \n2. **Library Scope:** `swesweep-fasthttp` (specifically the `fasthttpproxy` library) is a helper for configuring fasthttp's dialer. Fasthttp itself allows configuring `ReadTimeout` and `WriteTimeout` for the client. If the user configures fasthttp with appropriate timeouts, those timeouts might apply to the read/write operations depending on how the connection is managed, but the dialing process itself (the TCP handshake and CONNECT handshake) is distinct.\n3. **Attacker Reachability:** To trigger this, an attacker must control the proxy address (via `HTTP_PROXY` env var or config) and make it accept TCP but never reply. This is a ",
"elapsed": 6.2,
"tokens": 4581,
"tokens_in": 4264,
"tokens_out": 317,
"cost_usd": 0.00274188,
"file": "fasthttpproxy/dialer.go",
"round": 1
},
{
"finding_title": "Unbounded hang on CONNECT when ConnectTimeout is zero (default)",
"verdict": "VALID",
"reasoning": "The report claims that a zero-value `ConnectTimeout` (the default) leads to unbounded hangs when dialing through an HTTP proxy because `conn.SetDeadline` is not called. I verified the code in `fasthttpproxy/dialer.go`. In `httpProxyDial`, the code retrieves `connectTimeout` from the dialer. It explicitly checks `if connectTimeout > 0` before calling `conn.SetDeadline`. If `connectTimeout` is 0 (the zero value for `time.Duration`), this block is skipped, and no deadline is set on the connection before the `conn.Write` or `res.Read` calls. Consequently, a malicious or unresponsive proxy that accepts the TCP connection but never responds to the CONNECT request will cause the calling goroutine to block indefinitely. This aligns with the threat model for a library: a caller using the default configuration (zero `ConnectTimeout`) is exposed to a denial-of-service (goroutine/fd leak) by a malicious proxy server. The threat model explicitly lists \"resource bounds (unbounded allocation, algorithmic complexity, infinite loops on crafted input)\" and \"correct behaviour on malformed input\" as assets/impacts. While standard Go `net.Dial` also blocks indefinitely without a timeout, this library introduces a specific `ConnectTimeout` field specifically to mitigate this risk during the proxy handshake. Leaving it at zero (default) effectively disables the protection, leading to a predictable DoS vector when interacting with untrusted proxies. The fact that it's a \"default\" behavior makes it a significant security pitfall for library consumers who might not explicitly set the timeout, assuming safe defaults. Therefore, the lack of a default timeout for this specific handshake step is a valid security vulnerability in the context of a reusable library handling untrusted network inputs.\n\nCRUX: When d.ConnectTimeout is 0, conn.SetDeadline is never called in httpProxyDial, allowing a malicious proxy to block the goroutine indefinitely.\nGREP: if connectTimeout > 0",
"elapsed": 7.1,
"tokens": 5139,
"tokens_in": 4711,
"tokens_out": 428,
"cost_usd": 0.00326262,
"file": "fasthttpproxy/dialer.go",
"round": 2,
"grep_used": true,
"grep_results": "GREP `if connectTimeout > 0`:\n```\nfasthttpproxy/dialer.go:205:\tif connectTimeout > 0 {\n```"
},
{
"finding_title": "Unbounded hang on CONNECT when ConnectTimeout is zero (default)",
"verdict": "INVALID",
"reasoning": "The report claims that a zero `ConnectTimeout` leads to an unbounded hang when dialing through an HTTP proxy. The code in `httpProxyDial` confirms this: it checks `if connectTimeout > 0` before setting a deadline. If `ConnectTimeout` is 0 (the default), no deadline is set, and `conn.Write` or `res.Read` can block indefinitely if the proxy does not respond. However, this behavior is consistent with standard Go networking practices where `net.Dial` without a timeout blocks indefinitely. The threat model for this library does not explicitly require that all network operations have default timeouts; it focuses on memory safety, correct behavior on malformed input, and resource bounds. While an unresponsive proxy could cause a DoS, this is a standard operational concern rather than a security vulnerability in the context of a reusable library that allows users to configure timeouts. The library provides the `ConnectTimeout` field for users to set a timeout, which is sufficient defense. Therefore, the lack of a default timeout is not a security vulnerability under the given threat model.\n\nCRUX: The lack of a default timeout for the CONNECT handshake is a standard Go behavior and not a security vulnerability in the context of a reusable library.\nGREP: ConnectTimeout",
"elapsed": 4.8,
"tokens": 5446,
"tokens_in": 5173,
"tokens_out": 273,
"cost_usd": 0.0029916599999999997,
"file": "fasthttpproxy/dialer.go",
"round": 3,
"grep_used": true,
"grep_results": "GREP `ConnectTimeout`:\n```\nfasthttpproxy/dialer.go:69:\tConnectTimeout time.Duration\nfasthttpproxy/dialer.go:171:\treturn d.ConnectTimeout\nfasthttpproxy/http.go:32:\td := Dialer{Config: httpproxy.Config{HTTPProxy: proxy, HTTPSProxy: proxy}, Timeout: timeout, ConnectTimeout: timeout}\nfasthttpproxy/http.go:60:\t\tConfig: httpproxy.Config{HTTPProxy: proxy, HTTPSProxy: proxy}, Timeout: timeout, ConnectTimeout: timeout,\nfasthttpproxy/proxy_env.go:36:\td := Dialer{Timeout: timeout, ConnectTimeout: timeout}\n```"
},
{
"finding_title": "Unbounded hang on CONNECT when ConnectTimeout is zero (default)",
"verdict": "VALID",
"reasoning": "The report claims that when `ConnectTimeout` is 0 (the default for a zero-value `Dialer` struct), a malicious proxy can cause an unbounded hang by accepting the TCP connection but never replying to the CONNECT request. This leads to a denial-of-service via goroutine and file descriptor exhaustion. \n\n1. **Bug Existence**: The code in `httpProxyDial` (line ~205) explicitly checks `if connectTimeout > 0` before calling `conn.SetDeadline`. If `ConnectTimeout` is 0, no deadline is set. The subsequent `conn.Write` and `res.Read` operations are blocking I/O calls that can hang indefinitely on a black-hole network endpoint. Thus, the bug pattern (lack of timeout) exists in the code.\n\n2. **Attacker Reachability**: The threat model for a reusable library includes untrusted input reaching public APIs. The `Dialer` is constructed by the host application. If the application constructs the `Dialer` using the zero-value (e.g., `var d Dialer` or `Dialer{}`) without explicitly setting `ConnectTimeout`, it is vulnerable. The attacker controls the network endpoint (the proxy address), which is a standard vector for this type of library interaction. The attacker does not need to control the library's configuration, only the network traffic (accepting the connection and stalling). This satisfies the \"attacker-controlled input\" requirement for network-based DoS.\n\n3. **Defense Verification**: \n - Reviewer 3 argues that this is standard Go behavior. However, the library provides a specific `ConnectTimeout` field, implying an intent to manage this timeout. Relying on the user to remember to set a field to avoid a security vulnerability (DoS) is a common pitfall. More importantly, does any default exist? \n - Grep results show that `fasthttpproxy/http.go` and `proxy_env.go` set `ConnectTimeout: timeout` where `timeout` is an explicit parameter. However, the `Dialer` struct itself, when instantiated directly by a user (which is a supported public API usage, given it is an exported struct), defaults to 0.\n - The `fasthttp.TCPDialer` (embedded) has its own default timeout of 3 seconds (`DialTimeout`), but that applies to `Dial`/`DialTimeout` calls for the *initial TCP handshake*, not the *proxy CONNECT handshake* which happens inside `httpProxyDial` on the already-established connection. The `SetDeadline` in `httpProxyDial` is the only protection for the CONNECT phase. If it is not set, there is no protection.\n\n4. **Severity**: An unbounded hang on a network operation is a classic Denial of Service vector. Since the library allows the user to configure this timeout but defaults to \"no timeout\" (0), and since this is reachable by any user who interacts with an unresponsive/malicious proxy using the default struct initialization, this is a valid security vulnerability. The host process can be crashed or degraded by resource exhaustion.\n\n5. **Conclusion**: The bug is real, reachable, and causes meaningful harm (DoS). The lack of a default timeout in the public struct is a security flaw because it allows the default configuration to be unsafe.\n\nCRUX: When d.ConnectTimeout is 0, conn.SetDeadline is never called in httpProxyDial, allowing a malicious proxy to block the goroutine indefinitely.\nGREP: if connectTimeout > 0",
"elapsed": 12.4,
"tokens": 6359,
"tokens_in": 5592,
"tokens_out": 767,
"cost_usd": 0.00464964,
"file": "fasthttpproxy/dialer.go",
"round": 4,
"grep_used": true,
"grep_results": "GREP `if connectTimeout > 0`:\n```\nfasthttpproxy/dialer.go:205:\tif connectTimeout > 0 {\n```"
},
{
"finding_title": "Unbounded hang on CONNECT when ConnectTimeout is zero (default)",
"verdict": "INVALID",
"reasoning": "The reported vulnerability claims that a zero-value `ConnectTimeout` allows a malicious proxy to cause an unbounded hang in `httpProxyDial`. I verified that the code at line 205 in `fasthttpproxy/dialer.go` indeed only calls `conn.SetDeadline` if `connectTimeout > 0`. Thus, if the field is 0, no timeout is set for the CONNECT handshake. However, under the provided threat model for a reusable library, a crash or resource exhaustion is only considered a vulnerability if it is reachable through the public API with a crafted payload where the attacker controls the input violating an invariant or causing memory unsafety/corruption. Here, the 'input' is the network behavior of the proxy. The library provides a mechanism (`ConnectTimeout`) to mitigate this, but leaving it at the zero value (no timeout) is a standard configuration choice consistent with Go's `net` package defaults for blocking I/O when no timeout is specified. The threat model explicitly states that 'Misuse that violates a documented precondition of the public API' or issues that are 'code quality issues' are not vulnerabilities. More importantly, an unbounded hang due to a misconfigured timeout is generally classified as a robustness or operational issue rather than a security vulnerability in library code, unless it leads to memory corruption. Since the library correctly implements the timeout logic based on the user's configuration, and the zero value simply means 'no timeout' (standard semantics), there is no security flaw in the library itself. The user is expected to configure appropriate timeouts for their security posture. Therefore, this is INVALID.\n\nCRUX: Whether the lack of a default timeout in a library's configurable field constitutes a security vulnerability or is standard expected behavior requiring user configuration.\nGREP: ConnectTimeout",
"elapsed": 6.9,
"tokens": 6700,
"tokens_in": 6320,
"tokens_out": 380,
"cost_usd": 0.0037944,
"file": "fasthttpproxy/dialer.go",
"round": 5,
"grep_used": true,
"grep_results": "GREP `ConnectTimeout`:\n```\nfasthttpproxy/dialer.go:69:\tConnectTimeout time.Duration\nfasthttpproxy/dialer.go:171:\treturn d.ConnectTimeout\nfasthttpproxy/http.go:32:\td := Dialer{Config: httpproxy.Config{HTTPProxy: proxy, HTTPSProxy: proxy}, Timeout: timeout, ConnectTimeout: timeout}\nfasthttpproxy/http.go:60:\t\tConfig: httpproxy.Config{HTTPProxy: proxy, HTTPSProxy: proxy}, Timeout: timeout, ConnectTimeout: timeout,\nfasthttpproxy/proxy_env.go:36:\td := Dialer{Timeout: timeout, ConnectTimeout: timeout}\n```"
},
{
"verdict": "INVALID",
"reasoning": "[ARBITER] Under the provided threat model for a reusable library, the `ConnectTimeout` field is a configuration parameter, not untrusted input data. The threat model explicitly excludes 'misuse that violates a documented precondition of the public API'. The default zero-value (0) in Go for `time.Duration` conventionally indicates 'no timeout' (blocking indefinitely), which is standard behavior for network libraries (e.g., `net.Dial` without timeout blocks). A 'malicious proxy' is an external entity, not an attacker-controlled payload passed into the public API. Therefore, the lack of a default timeout when the user does not configure one is a design choice/usability issue, not a security vulnerability where an attacker exploits the library's handling of untrusted data to cause resource exhaustion. The user is expected to configure timeouts to protect against unresponsive proxies; failing to do so is a misconfiguration on the host application side, not a flaw in the library's validation of inputs.",
"round": 6,
"file": "fasthttpproxy/dialer.go",
"finding_title": "Unbounded hang on CONNECT when ConnectTimeout is zero (default)",
"elapsed": 5.1,
"tokens_in": 4155,
"tokens_out": 208,
"cost_usd": 0.0023691
}
]Raw scan report for this file
Looking at this file, I need to trace untrusted data through the proxy dialing path: `addr` (target from request URLs), `proxyAddr`/`auth` (from proxy URL config/env), and proxy server responses.
**Key data flows:**
- `addr` → CONNECT request body (`httpProxyDial`) — CRLF-injection vector
- `pu.User` → base64 → `auth` → `Proxy-Authorization` header
- `pu.Host + pu.Path` → `proxyAddr` → TCP dial target
- Proxy response → `res.Read(bufio.NewReaderSize(conn, 1024))`
**Critical check — CRLF injection in CONNECT request:**
```go
if strings.ContainsAny(addr, "\r\n") { return nil, ... }
```
This correctly blocks the primary injection vector. The `auth` field is base64-encoded (`[A-Za-z0-9+/=]`), so no CRLF can be smuggled through it. Good.
**However, there are residual issues:**
1. **`proxyAddr` is not validated.** It's derived from `pu.Host + pu.Path` and passed to `dialer.Dial(network, proxyAddr)`. If a proxy URL like `http://proxy:8080/../malicious` is crafted, the path component leaks into the socket address. This only affects the TCP dial (not the CONNECT request), so impact is limited to dial failures, but it means the `pu.Path` component has no validation.
2. **`bufio.Reader` buffer data loss → protocol desync.** After `res.Read(...)` succeeds with status 200, the `bufio.Reader` is discarded. If a malicious proxy sends the `200` response headers *and* the first bytes of the tunneled connection (e.g., TLS ServerHello) in a single TCP segment, the tunneled bytes sit in the `bufio.Reader`'s internal buffer. When the caller reads from the raw `net.Conn`, those bytes are gone — the TLS handshake desyncs. A malicious or compromised proxy can trigger this deterministically to cause 100% connection failure (DoS).
3. **No timeout on CONNECT when `ConnectTimeout == 0` (the default).** A malicious proxy can accept the TCP connection and then never reply to the CONNECT request. With no deadline set, the goroutine blocks indefinitely, leaking connections and potentially exhausting the host's file descriptors.
4. **Heuristic TLS detection** via `strings.HasSuffix(addr, ":443")` picks the wrong proxy (HTTP vs HTTPS) for any TLS service on a non-443 port or non-TLS service on 443. An attacker controlling target URLs could steer traffic through an unintended proxy, but this is a behavioral bug rather than a memory-safety issue.
The `sync.Map` type assertion `v.(*proxyInfo)` is safe because the map is created locally in the closure and only written by `addrAndAuth`, which only stores `*proxyInfo`.
```json
[
{
"severity": "medium",
"title": "Protocol desync: bufio.Reader discards tunneled data after CONNECT 200",
"function": "httpProxyDial",
"description": "After res.Read(bufio.NewReaderSize(conn, 1024)) returns a 200 status, the bufio.Reader is discarded while the raw net.Conn is returned to the caller. If a malicious proxy sends the HTTP 200 response headers and the first bytes of the tunneled connection (e.g. TLS ServerHello) in a single TCP segment, those tunnel bytes remain in the bufio.Reader's internal buffer and are silently dropped. The caller then sends its TLS ClientHello to a proxy that already has the ServerHello in-flight, causing a guaranteed handshake failure. A malicious or compromised proxy can trigger this deterministically to DoS all connections routed through it."
},
{
"severity": "medium",
"title": "Unbounded hang on CONNECT when ConnectTimeout is zero (default)",
"function": "httpProxyDial",
"description": "When d.ConnectTimeout is 0 (the zero-value default), conn.SetDeadline is never called before writing the CONNECT request. A malicious or unresponsive proxy that accepts the TCP connection but never replies to CONNECT will cause the calling goroutine to block indefinitely on conn.Write or the subsequent res.Read. This leaks file descriptors and goroutines, enabling a denial-of-service against the host process without any attacker-controlled payload."
},
{
"severity": "low",
"title": "Unvalidated proxy path component injected into socket address",
"function": "addrAndAuth",
"description": "proxyAddr is constructed as pu.Host + pu.Path with no sanitization of pu.Path. A proxy URL such as http://proxy:8080/forward yields proxyAddr = \"proxy:8080/forward\", which is passed verbatim to dialer.Dial(network, proxyAddr). The path suffix makes the address unresolvable, causing a dial failure. If the proxy URL is attacker-controllable (via HTTP_PROXY env or Dialer.Config), an attacker can force all proxy-routed connections to fail. The same unvalidated path also flows into the error message logged on non-200 responses."
},
{
"severity": "low",
"title": "TLS/HTTP proxy selection based on ':443' port suffix heuristic",
"function": "GetDialFunc (slow-path closure)",
"description": "The slow path determines whether to query the HTTP or HTTPS proxy via strings.HasSuffix(addr, \":443\"). Any TLS service listening on a non-443 port (e.g. :8443) is routed through the HTTP proxy, and any non-TLS service on :443 is routed through the HTTPS proxy. An attacker who can influence target URLs (e.g. via an open redirect or crafted link) can steer plaintext or TLS traffic through an unintended proxy, potentially bypassing proxy-level access controls or leaking sensitive data to an untrusted intermediary."
}
]
```