TLS/HTTP proxy selection based on ':443' port suffix heuristic
low swesweep-fasthttp · fasthttpproxy/dialer.go · GetDialFunc (slow-path closure) · run #4
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.
Triage
Not triaged.
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."
}
]
```