BudgetScan

Data race on caller-supplied dst buffer after request timeout

high swesweep-fasthttp · client.go · clientGetURLDeadline · run #4

Description

When the deadline timer fires, the function returns dst to the caller while the spawned goroutine may still be writing the server response body into dst via doRequestFollowRedirectsBuffer (bodyBuf.B = dst). The sync.Mutex only guards the timedout/responded flags and channel send, not the dst buffer. An attacker controlling the remote server can trigger this by sending a slow, large response body that causes the client to time out mid-transfer. The caller then reads a buffer that is concurrently being mutated, leading to data corruption, injection of attacker-controlled content into the caller's processing pipeline, or potential memory corruption.

Triage

Verdict
VALID confidence 0.83
Model
qwen/qwen3.8-27b
Rounds
6
Cost
$0.0779 · 384.6 s
[ARBITER] The code contains a genuine data race on the caller-supplied `dst` buffer. In `clientGetURLDeadline`, a background goroutine is spawned to execute `doRequestFollowRedirectsBuffer(req, dst, url, c)`. Inside `doRequestFollowRedirectsBuffer`, the response body buffer is set to point directly to the caller's `dst` slice (`bodyBuf.B = dst`). The main goroutine uses a `select` statement to wait for either the background goroutine's completion (via a channel) or a timeout. If the timeout fires first, the main goroutine returns `dst` to the caller immediately. However, the background goroutine is not cancelled and continues to execute `doRequestFollowRedirectsBuffer`, which writes the HTTP response body into `dst` via the `bodyBuf`. Consequently, the caller may read from `dst` (which was returned on timeout) while the background goroutine is concurrently writing to it. This constitutes a data race, leading to undefined behavior, potential memory corruption, and data integrity issues, which are valid security vulnerabilities for a reusable library.
All rounds
[
  {
    "finding_title": "Data race on caller-supplied dst buffer after request timeout",
    "verdict": "UNCERTAIN",
    "reasoning": "The report claims a data race occurs on the caller-supplied `dst` buffer when a timeout happens in `GetTimeout`/`GetDeadline`. \n\n1. **Trace the code path:** `GetTimeout` calls `clientGetURLTimeout`, which calls `clientGetURLDeadline`. \n2. **Inside `clientGetURLDeadline`:** A goroutine is spawned: `go func() { ... doRequestFollowRedirectsBuffer(req, dst, url, c) ... }()`. The main goroutine waits on a `select` for either a response from a channel or a timeout. \n3. **On Timeout:** If the timeout fires first (`case <-tc.C`), the main goroutine sets `timedout = true` and returns `body = dst`. Crucially, the spawned goroutine is **not** canceled. It continues executing `doRequestFollowRedirectsBuffer`. \n4. **Inside `doRequestFollowRedirectsBuffer`:** The function sets `bodyBuf.B = dst` (where `bodyBuf` is the response's internal buffer). It then calls `doRequestFollowRedirects`, which eventually calls `resp.ReadLimitBody`. This parses the HTTP response and writes the body bytes directly into the `bodyBuf.B` (which is `dst`) via `br.Read(resp.body...)` or similar mechanisms in the `Response` parsing logic. \n5. **Race Condition:** While the main goroutine returns `dst` to the caller (who may then read it), the background goroutine is still actively reading from the network connection and writing server-controlled bytes into `dst`. This is a classic ",
    "elapsed": 32.4,
    "tokens": 27349,
    "tokens_in": 27001,
    "tokens_out": 348,
    "cost_usd": 0.01238442,
    "file": "client.go",
    "round": 1
  },
  {
    "finding_title": "Data race on caller-supplied dst buffer after request timeout",
    "verdict": "VALID",
    "reasoning": "The report claims a data race on the caller-supplied `dst` buffer in `clientGetURLDeadline` (used by `GetTimeout`/`GetDeadline`) when the timeout fires. \n\n1. **Bug Existence**: The code spawns a goroutine that calls `doRequestFollowRedirectsBuffer(req, dst, url, c)`. Inside this function, `bodyBuf.B = dst` is set, meaning the HTTP response body is written directly into the caller's `dst` slice. The main goroutine uses a `select` statement. If the timeout case (`< -tc.C`) is hit, the main goroutine sets `timedout = true` and returns `body = dst`. The spawned goroutine is *not* cancelled; it continues to execute. If the server is slow (e.g., sending a large body slowly), the background goroutine will still be executing `resp.ReadLimitBody`, which reads from the network connection and appends data to `bodyBuf.B` (which is `dst`). \n\n2. **Attacker Reachability**: The input `dst` is provided by the application developer calling `GetTimeout`. However, the *trigger* for the race is the *remote server's* behavior. The threat model for a library client identifies the remote server as an untrusted input source. An attacker (controlling the remote server) can craft a response that takes longer than the specified `timeout` to arrive. This causes the main goroutine to time out and return `dst` to the caller while the background goroutine is still writing attacker-controlled bytes into `dst`. \n\n3. **Harm**: The caller receives a reference to `dst`. If the caller then reads from `dst` (e.g., parses the JSON/HTML body, or displays it), they are reading from a memory location that is being concurrently written to by the background goroutine. This is a data race. In Go, data races are undefined behavior and can lead to torn reads (partial data corruption), but more critically, if `dst` is a reused buffer or if the application relies on the integrity of the data for security decisions (e.g., authorization checks, input validation), this can lead to security bypasses or logic errors. Furthermore, if the background goroutine writes to `dst` after the caller has moved on, it could corrupt data in a buffer that is now being used for a different purpose (if `dst` is not a fresh allocation). Even with a fresh allocation, the concurrent read/write is a violation of memory safety in Go (UB). \n\n4. **Defense**: The `sync.Mutex` in `clientGetURLDeadline` only protects the `timedout` and `responded` flags and the channel communication. It does *not* protect the `dst` buffer. There is no mechanism to stop the background goroutine from writing to `dst` after the timeout. The `net.Conn` might eventually time out via `ReadTimeout`, but that is separate from the `GetTimeout` deadline and may not happen immediately. \n\n5. **Conclusion**: The data race is real. It is triggered by untrusted input (remote server response timing/size). It leads to undefined behavior/concurrent access to caller-supplied memory, which is a security vulnerability in a library context (integrity of the host process).\n\nCRUX: The background goroutine continues to write to `dst` (via `bodyBuf.B = dst`) even after the main goroutine times out and returns `dst` to the caller, creating a data race on untrusted input-triggered execution.\nGREP: bodyBuf.B = dst",
    "elapsed": 137.1,
    "tokens": 28250,
    "tokens_in": 27477,
    "tokens_out": 773,
    "cost_usd": 0.01385934,
    "file": "client.go",
    "round": 2,
    "grep_used": true,
    "grep_results": "GREP `= dst`:\n```\nbytesconv_test.go:456:\tresult = dst[len(prefix):]\nstackless/writer.go:88:\tw.dstW = dstW\nbytesconv.go:83:\tdst = dst.To4() // dst is always non-nil here\nuri.go:625:\tdst = dst[:0]\nuri.go:630:\tb := dst\nuri.go:642:\tdst = dst[:bSize]\nuri.go:645:\tb = dst\nuri.go:745:\tu.requestURI = dst\nclient.go:1097:\t\t\tbody = dst\nclient.go:1147:\tbodyBuf.B = dst\nargs.go:388:\t\tdst = dst[:cap(dst)] // copy all of dst.\nargs.go:390:\t\tfor i := dstLen; i < len(tmp); i++ {\nargs.go:398:\tdst = dst[:n]\nargs.go:404:\t\t\tdstKV.value = dstKV.value[:0]\nheader.go:3516:\t\tdst = dst[:len(src)]\nheader.go:3524:\t\t\tdst[i] = dst[i][:l]\nhttp_test.go:170:\tif src.String() != dst.String() || !bytes.Equal(src.Body(), dst.Body()) {\nhttp.go:2531:\t\t\tnw, ew := dst.Write(buf[0:nr])\nhttp.go:2622:\tdst = dst[:0]\nhttp.go:2644:\tdst = dst[:cap(dst)]\nhttp.go:2689:\tdst = dst[:dstLen]\nhttp.go:2704:\t\tif offset == dstLen {\nhttp.go:2743:\t\tdst = dst[:len(dst)-strCRLFLen]\n```"
  },
  {
    "finding_title": "Data race on caller-supplied dst buffer after request timeout",
    "verdict": "VALID",
    "reasoning": "The reported vulnerability is a data race on the caller-supplied `dst` buffer in `clientGetURLDeadline`. The code spawns a goroutine that calls `doRequestFollowRedirectsBuffer`, which sets the internal response body buffer (`bodyBuf`) to point to `dst` (`bodyBuf.B = dst`). The main goroutine uses a `select` to wait for either the response or a timeout. If the timeout fires first, the main goroutine returns `dst` to the caller. However, the background goroutine is not cancelled and continues to read from the network, writing data into `dst`. This creates a concurrent read (by the caller) and write (by the background goroutine) on the same memory region, which is undefined behavior in Go and constitutes a memory safety violation. An attacker controlling the remote server can trigger this by sending a response body slowly enough to cause the client-side timeout, while continuing to stream data that the background goroutine writes into the buffer. This leads to data corruption in the host process.\n\nCRUX: The background goroutine continues to write to `dst` (via `bodyBuf.B = dst`) even after the main goroutine times out and returns `dst` to the caller, creating a data race on untrusted input-triggered execution.\nGREP: bodyBuf.B = dst",
    "elapsed": 49.3,
    "tokens": 28930,
    "tokens_in": 28644,
    "tokens_out": 286,
    "cost_usd": 0.012888479999999999,
    "file": "client.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `= dst`:\n```\nstackless/writer.go:88:\tw.dstW = dstW\nbytesconv_test.go:456:\tresult = dst[len(prefix):]\nuri.go:625:\tdst = dst[:0]\nuri.go:630:\tb := dst\nuri.go:642:\tdst = dst[:bSize]\nuri.go:645:\tb = dst\nuri.go:745:\tu.requestURI = dst\nclient.go:1097:\t\t\tbody = dst\nclient.go:1147:\tbodyBuf.B = dst\nargs.go:388:\t\tdst = dst[:cap(dst)] // copy all of dst.\nargs.go:390:\t\tfor i := dstLen; i < len(tmp); i++ {\nargs.go:398:\tdst = dst[:n]\nargs.go:404:\t\t\tdstKV.value = dstKV.value[:0]\nheader.go:3516:\t\tdst = dst[:len(src)]\nheader.go:3524:\t\t\tdst[i] = dst[i][:l]\nbytesconv.go:83:\tdst = dst.To4() // dst is always non-nil here\nhttp_test.go:170:\tif src.String() != dst.String() || !bytes.Equal(src.Body(), dst.Body()) {\nhttp.go:2531:\t\t\tnw, ew := dst.Write(buf[0:nr])\nhttp.go:2622:\tdst = dst[:0]\nhttp.go:2644:\tdst = dst[:cap(dst)]\nhttp.go:2689:\tdst = dst[:dstLen]\nhttp.go:2704:\t\tif offset == dstLen {\nhttp.go:2743:\t\tdst = dst[:len(dst)-strCRLFLen]\n```"
  },
  {
    "finding_title": "Data race on caller-supplied dst buffer after request timeout",
    "verdict": "VALID",
    "reasoning": "The reported vulnerability is a data race on the caller-supplied `dst` buffer in `clientGetURLDeadline` (and consequently `GetTimeout`/`GetDeadline`). The code spawns a background goroutine to execute `doRequestFollowRedirectsBuffer(req, dst, url, c)`. Inside this function, `bodyBuf.B = dst` sets the response body buffer to point directly to the caller's `dst` slice. The main goroutine waits for either a response or a timeout. If the timeout fires, the main goroutine returns `body = dst` to the caller. However, the background goroutine is not canceled and continues to read from the network connection, writing server-controlled bytes into `dst` via `resp.ReadLimitBody`. Since the main goroutine has already returned `dst` to the caller, any subsequent read by the caller races with the concurrent write by the background goroutine. In Go, this is undefined behavior and can lead to data corruption, torn reads, or memory safety issues. An attacker controlling the remote server can easily trigger this by sending a response body slowly enough to cause the client-side timeout while continuing to stream data. This is a valid security vulnerability as it allows an external attacker to cause memory corruption or data integrity issues in the host process.\n\nCRUX: The background goroutine continues to write to `dst` (via `bodyBuf.B = dst`) even after the main goroutine times out and returns `dst` to the caller, creating a data race on untrusted input-triggered execution.\nGREP: bodyBuf.B = dst",
    "elapsed": 80.5,
    "tokens": 29334,
    "tokens_in": 28996,
    "tokens_out": 338,
    "cost_usd": 0.013192319999999999,
    "file": "client.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `= dst`:\n```\nbytesconv_test.go:456:\tresult = dst[len(prefix):]\nstackless/writer.go:88:\tw.dstW = dstW\nuri.go:625:\tdst = dst[:0]\nuri.go:630:\tb := dst\nuri.go:642:\tdst = dst[:bSize]\nuri.go:645:\tb = dst\nuri.go:745:\tu.requestURI = dst\nbytesconv.go:83:\tdst = dst.To4() // dst is always non-nil here\nclient.go:1097:\t\t\tbody = dst\nclient.go:1147:\tbodyBuf.B = dst\nargs.go:388:\t\tdst = dst[:cap(dst)] // copy all of dst.\nargs.go:390:\t\tfor i := dstLen; i < len(tmp); i++ {\nargs.go:398:\tdst = dst[:n]\nargs.go:404:\t\t\tdstKV.value = dstKV.value[:0]\nheader.go:3516:\t\tdst = dst[:len(src)]\nheader.go:3524:\t\t\tdst[i] = dst[i][:l]\nhttp_test.go:170:\tif src.String() != dst.String() || !bytes.Equal(src.Body(), dst.Body()) {\nhttp.go:2531:\t\t\tnw, ew := dst.Write(buf[0:nr])\nhttp.go:2622:\tdst = dst[:0]\nhttp.go:2644:\tdst = dst[:cap(dst)]\nhttp.go:2689:\tdst = dst[:dstLen]\nhttp.go:2704:\t\tif offset == dstLen {\nhttp.go:2743:\t\tdst = dst[:len(dst)-strCRLFLen]\n```"
  },
  {
    "finding_title": "Data race on caller-supplied dst buffer after request timeout",
    "verdict": "VALID",
    "reasoning": "The report correctly identifies a data race on the caller-supplied `dst` buffer in `clientGetURLDeadline` (used by `GetTimeout`/`GetDeadline`). The code spawns a goroutine that writes to `dst` via `doRequestFollowRedirectsBuffer` (which sets `bodyBuf.B = dst`). If the main goroutine's timeout fires before the background goroutine completes, the main goroutine returns `dst` to the caller while the background goroutine continues writing to it. This constitutes a data race, which is undefined behavior in Go and can lead to memory corruption or data integrity issues. The attacker controls the remote server's response speed, allowing them to trigger this race by sending a slow or large response. Therefore, the vulnerability is real and attacker-reachable.\n\nCRUX: The background goroutine continues to write to `dst` after the main goroutine has timed out and returned `dst` to the caller, creating a data race.\nGREP: bodyBuf.B = dst",
    "elapsed": 73.3,
    "tokens": 29614,
    "tokens_in": 29393,
    "tokens_out": 221,
    "cost_usd": 0.01300806,
    "file": "client.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `= dst`:\n```\nbytesconv_test.go:456:\tresult = dst[len(prefix):]\nstackless/writer.go:88:\tw.dstW = dstW\nuri.go:625:\tdst = dst[:0]\nuri.go:630:\tb := dst\nuri.go:642:\tdst = dst[:bSize]\nuri.go:645:\tb = dst\nuri.go:745:\tu.requestURI = dst\nbytesconv.go:83:\tdst = dst.To4() // dst is always non-nil here\nclient.go:1097:\t\t\tbody = dst\nclient.go:1147:\tbodyBuf.B = dst\nargs.go:388:\t\tdst = dst[:cap(dst)] // copy all of dst.\nargs.go:390:\t\tfor i := dstLen; i < len(tmp); i++ {\nargs.go:398:\tdst = dst[:n]\nargs.go:404:\t\t\tdstKV.value = dstKV.value[:0]\nheader.go:3516:\t\tdst = dst[:len(src)]\nheader.go:3524:\t\t\tdst[i] = dst[i][:l]\nhttp_test.go:170:\tif src.String() != dst.String() || !bytes.Equal(src.Body(), dst.Body()) {\nhttp.go:2531:\t\t\tnw, ew := dst.Write(buf[0:nr])\nhttp.go:2622:\tdst = dst[:0]\nhttp.go:2644:\tdst = dst[:cap(dst)]\nhttp.go:2689:\tdst = dst[:dstLen]\nhttp.go:2704:\t\tif offset == dstLen {\nhttp.go:2743:\t\tdst = dst[:len(dst)-strCRLFLen]\n```"
  },
  {
    "verdict": "VALID",
    "reasoning": "[ARBITER] The code contains a genuine data race on the caller-supplied `dst` buffer. In `clientGetURLDeadline`, a background goroutine is spawned to execute `doRequestFollowRedirectsBuffer(req, dst, url, c)`. Inside `doRequestFollowRedirectsBuffer`, the response body buffer is set to point directly to the caller's `dst` slice (`bodyBuf.B = dst`). The main goroutine uses a `select` statement to wait for either the background goroutine's completion (via a channel) or a timeout. If the timeout fires first, the main goroutine returns `dst` to the caller immediately. However, the background goroutine is not cancelled and continues to execute `doRequestFollowRedirectsBuffer`, which writes the HTTP response body into `dst` via the `bodyBuf`. Consequently, the caller may read from `dst` (which was returned on timeout) while the background goroutine is concurrently writing to it. This constitutes a data race, leading to undefined behavior, potential memory corruption, and data integrity issues, which are valid security vulnerabilities for a reusable library.",
    "round": 6,
    "file": "client.go",
    "finding_title": "Data race on caller-supplied dst buffer after request timeout",
    "elapsed": 12.0,
    "tokens_in": 28094,
    "tokens_out": 242,
    "cost_usd": 0.012525479999999999
  }
]
Raw scan report for this file

# Security Analysis: `client.go` (fasthttp)

## Data Flow Tracing

I traced untrusted input through the public API surface:

- **`url`** → `hostnameFromURLString` → `host` → `c.hostClient` → `HostClient.Addr`
- **`req`** → `req.SetRequestURI` / `parseURI` → `req.URI().Host()` → connection target
- **`location`** (server response header) → `getRedirectURL` → `dst.UpdateBytes(location)` → next `url`
- **`postArgs`** → `req.BodyWriter`
- **Server response bytes** → `resp.Read` / `ReadLimitBody` → `dst` buffer

---

## Findings

### 1. Data race on `dst` buffer after timeout in `clientGetURLDeadline`

**Function:** `clientGetURLDeadline` (reachable via `GetDeadline`, `GetTimeout`, `Client.GetDeadline`, `Client.GetTimeout`, `HostClient.GetDeadline`, `HostClient.GetTimeout`)

**Flow:**
```
caller passes dst []byte
  → clientGetURLDeadline(dst, url, deadline, c)
    → spawns goroutine: doRequestFollowRedirectsBuffer(req, dst, url, c)
      → bodyBuf.B = dst          // response body written directly into dst
      → doRequestFollowRedirects → c.Do → transport.RoundTrip
        → resp.ReadLimitBody(br, maxBody)  // reads server body INTO dst
    → caller waits on select { ch | timer }
    → if timer fires: returns body = dst to caller
    → goroutine MAY STILL BE WRITING TO dst
```

The goroutine writes the response body directly into the caller-supplied `dst` slice (`bodyBuf.B = dst`). When the deadline timer fires, the main goroutine sets `timedout = true` and returns `body = dst` to the caller. The spawned goroutine is **not cancelled** — it continues executing `doRequestFollowRedirectsBuffer`, which is still reading the server's response body into `dst`. The code comment explicitly acknowledges this:

> "Note that the request continues execution on ErrTimeout until client-specific ReadTimeout exceeds."

The `sync.Mutex` only guards the `timedout`/`responded` flags and the channel send — it does **not** protect the `dst` buffer itself. This creates a concurrent read (by the caller) and write (by the goroutine) on the same `[]byte` without synchronization.

**Exploitability:** An attacker controlling the remote server can trigger this by:
1. Returning a response with a large `Content-Length` (or chunked body)
2. Sending body data slowly so the client's deadline expires mid-transfer
3. Continuing to send body data after the timeout

The caller receives a partially-filled `dst` that is still being mutated. If the caller parses `dst` (e.g., as JSON, form data, or a protocol message), the attacker can inject corrupted or malicious content. In the worst case, if `dst` is a fixed-size buffer and the goroutine writes beyond the expected boundary, this could corrupt adjacent memory.

**Severity: High** — memory integrity / data corruption, attacker-controllable via slow response.

---

### 2. Nil pointer dereference in public `Do*` methods

**Functions:** `Do`, `DoTimeout`, `DoDeadline`, `DoRedirects` (package-level and on `Client`/`HostClient`)

**Flow:**
```
Do(req, resp)
  → defaultClient.Do(req, resp)
    → uri := req.URI()              // PANIC: nil pointer dereference
```

```
DoTimeout(req, resp, timeout)
  → req.timeout = timeout           // PANIC: nil pointer dereference
```

```
DoDeadline(req, resp, deadline)
  → req.timeout = time.Until(deadline)  // PANIC: nil pointer dereference
```

```
DoRedirects(req, resp, n)
  → req.URI().DisablePathNormalizing = true  // PANIC: nil pointer dereference
```

Every public `Do*` entry point dereferences `req` without a nil check. If `req` is nil, the host process crashes with a runtime panic. While the documentation states "Request must contain at least non-zero RequestURI," the functions are exported library APIs. In a host application that processes untrusted data (e.g., a proxy, API gateway, or web crawler), a code path that produces a nil `*Request` (from a failed parse, a nil map lookup, etc.) will crash the entire process.

The `doNonNilReqResp` method contains an explicit debug panic for nil `req`/`resp`, confirming the developers are aware of this risk but chose to panic rather than return an error.

**Severity: Medium** — process crash (DoS). Not directly attacker-controllable unless the host application has a bug that produces nil requests from untrusted data.

---

### 3. `AddMissingPort` mishandles unbracketed IPv6 literals

**Function:** `AddMissingPort`

```go
func AddMissingPort(addr string, isTLS bool) string {
	...
	isIP6 := addr[0] == '['
	if isIP6 {
		// handles [::1] correctly
	} else { // IPv4
		columnPos := strings.LastIndexByte(addr, ':')
		if columnPos > 0 {
			return addr  // BUG: treats unbracketed IPv6 as having a port
		}
	}
	...
}
```

For an unbracketed IPv6 address like `::1` (no port, no brackets), `addr[0]` is `:` (not `[`), so it enters the "IPv4" branch. `strings.LastIndexByte("::1", ':')` returns `2`, which is `> 0`, so the function returns the address as-is **without appending a port**. The dialer then receives `::1` with no port, which will fail to dial or dial an unintended address.

While unbracketed IPv6 literals are non-standard, they can appear in configuration files or user-supplied addresses. This is a correctness bug rather than a direct security vulnerability, but it could cause connection failures or, in edge cases, connection to an unintended port.

**Severity: Low** — incorrect port handling for non-standard input.

---

## Functions Reviewed and Found Safe

- **`stripSensitiveHeadersOnRedirect` / `shouldStripSensitiveHeadersOnRedirect` / `isDomainOrSubdomainBytes`**: The subdomain check correctly uses `EqualFold` and requires a `.` separator. No bypass via encoding tricks. Sensitive headers (Authorization, Cookie, etc.) are properly stripped on cross-domain redirects.
- **`splitHostPortBytes` / `hostnameFromHostPortBytes`**: Correctly handles bracketed IPv6. The unbracketed IPv6 edge case in `splitHostPortBytes` (multiple colons → no port split) is consistent with `AddMissingPort` behavior.
- **`hostClient`**: Double-checked locking with `mLock` is correct. `mOnce.Do` guarantees map initialization before use.
- **`AcquireConn` / `ReleaseConn` / `decConnsCount`**: Connection pool operations are protected by `connsLock`. The `wantConnQueue` is properly managed.
- **`connsCleaner` / `mCleaner`**: Goroutine lifecycle is managed correctly with proper lock acquisition.
- **`PipelineClient.Do` overflow handling**: Correctly evicts oldest work item and returns `ErrPipelineOverflow`.
- **`transport.RoundTrip`**: Properly handles timeouts, retries, and connection lifecycle. `MaxResponseBodySize` bounds body reads.
- **`tlsServerName` / `newClientTLSConfig`**: The `InsecureSkipVerify = true` path is only triggered by malformed addresses set by the application developer, not by attacker input.
- **`dialHostHard`**: Attempts all configured addresses before giving up. Proper deadline enforcement.

```json
[
  {
    "severity": "high",
    "title": "Data race on caller-supplied dst buffer after request timeout",
    "function": "clientGetURLDeadline",
    "description": "When the deadline timer fires, the function returns dst to the caller while the spawned goroutine may still be writing the server response body into dst via doRequestFollowRedirectsBuffer (bodyBuf.B = dst). The sync.Mutex only guards the timedout/responded flags and channel send, not the dst buffer. An attacker controlling the remote server can trigger this by sending a slow, large response body that causes the client to time out mid-transfer. The caller then reads a buffer that is concurrently being mutated, leading to data corruption, injection of attacker-controlled content into the caller's processing pipeline, or potential memory corruption."
  },
  {
    "severity": "medium",
    "title": "Nil pointer dereference panics in all public Do* entry points",
    "function": "Do, DoTimeout, DoDeadline, DoRedirects (package-level, Client, HostClient)",
    "description": "All public Do* methods dereference req without a nil check: req.URI() in Do, req.timeout assignment in DoTimeout/DoDeadline, and req.URI().DisablePathNormalizing in DoRedirects. Passing a nil *Request causes an unrecoverable runtime panic that crashes the host process. In library consumers that process untrusted data (proxies, API gateways, crawlers), a nil request produced by a failed parse or lookup leads to denial of service. doNonNilReqResp confirms awareness via an explicit debug panic but provides no error return at the public API boundary."
  },
  {
    "severity": "low",
    "title": "AddMissingPort fails to append default port for unbracketed IPv6 literals",
    "function": "AddMissingPort",
    "description": "For an unbracketed IPv6 address like '::1' (no square brackets, no port), the function enters the IPv4 branch where LastIndexByte finds a colon at position > 0 and returns the address unchanged without appending :80 or :443. The resulting address lacks a port, causing dial failures or unintended port selection. While non-standard, such addresses can appear in user-supplied configuration or upstream host lists."
  }
]
```