Security: request private channel for a low-severity DoS report
Security Report — Unbounded HTTP response-body read in gobuster (memory-exhaustion DoS)
| -- | -- Project | gobuster (github.com/OJ/gobuster) Version reviewed | v3.8.2 Commit | c77583fbb8824058d74d38240896eff610683c8e (2026-01-13) Component | libgobuster/http.go → HTTPClient.Request() Class | CWE-400 Uncontrolled Resource Consumption / CWE-770 Allocation of Resources Without Limits Impact | Denial of service (memory exhaustion) of the gobuster process Attacker | The scan target (a malicious or compromised host the operator points gobuster at) Proposed severity | Low (client-side CLI, operator-chosen target — see "Impact & honest severity")vhost is the realistic attack surface: the operator points gobuster at a URL they are testing, and the server operating at that URL fully controls the response. In vhost mode the impact is amplified two ways:
- During
PreRun, the default-vhost body and a random-vhost body are each read in full and retained for the entire scan asv.normalBody/v.abnormalBody(gobustervhost.go:108,124). - Every word then issues another
ReturnBody: truerequest (:173), and gobuster runs many worker threads concurrently, so peak allocation scales with the thread count.
s3/gcs additionally pass the unbounded body straight into xml.Unmarshal /
json.Unmarshal (GCS into a map[string]interface{}), which roughly doubles the
transient footprint and adds a parser-side amplification vector.
Root cause
The response body is untrusted input controlled by the scan target, but it is read
without any upper bound. http.Client.Timeout limits how long the read may run, but
within that window a fast server can deliver an amount of data limited only by
throughput — which then all lands in a single []byte.
Proof of concept
The following faithfully reproduces the exact vulnerable pattern (io.ReadAll on a
target-controlled body under a gobuster-style 10 s client timeout) and contrasts it
with the proposed fix. It does not require building gobuster itself.
// malicious server streams an oversized body; victim mirrors libgobuster/http.go
func vulnerableRead(body io.Reader) (int, float64) {
b, _ := io.ReadAll(body) // current gobuster behaviour
return len(b), heapMiB()
}
func fixedRead(body io.Reader, max int64) (int, float64) {
b, _ := io.ReadAll(io.LimitReader(body, max)) // proposed fix
return len(b), heapMiB()
}
Measured results (Go 1.22, loopback, single request, 10 s client timeout):
Simulated malicious target streams: 500 MiB
baseline heap: 0.1 MiB
[VULNERABLE io.ReadAll] buffered 500 MiB into memory, heapAlloc=561.4 MiB
[FIXED io.LimitReader 10] buffered 10 MiB into memory, heapAlloc= 19.2 MiB
A separate throughput measurement on loopback showed ~2,490 MiB/s, i.e. within gobuster's 10 s request-timeout window a single unbounded read could buffer on the order of ~24 GiB. Even on a modest ~100 MiB/s LAN that is ~1 GiB per request, multiplied by the worker-thread count. The result is an out-of-memory kill of the gobuster process (and potential memory pressure on the host).
Impact & honest severity
This is a client-side tool. The "victim" is the operator running gobuster, who has deliberately pointed it at a target; the "attacker" is that target's operator. The realistic outcome is that a malicious or compromised host being enumerated can crash the operator's gobuster process (and, with enough threads, stress the operator's machine). There is no code execution and no impact on third parties.
Because of that threat model I propose Low severity, not the Medium (~6.5) that
comparable server-side instances of this bug class received (e.g. Tekton
CVE-2026-40924, where the vulnerable process was a shared multi-tenant cluster pod).
It is nonetheless a genuine robustness/DoS defect worth fixing, and it fixes cleanly.
Remediation
Introduce a maximum response-body size and enforce it on the ReturnBody path:
// package-level default; ideally also expose as a CLI flag e.g. --max-response-size
const maxResponseBodyBytes = 100 << 20 // 100 MiB
if opts.ReturnBody {
body, err = io.ReadAll(io.LimitReader(resp.Body, maxResponseBodyBytes))
if err != nil {
return 0, 0, nil, nil, fmt.Errorf("could not read body %w", err)
}
length = int64(len(body))
}
Recommended refinements:
- Make the cap configurable (flag / option), defaulting to a sane value.
- Optionally detect truncation (read
max+1, and surface a clear "response exceeded limit" condition) so results are not silently based on a partial body. - For s3/gcs, prefer a streaming decoder over the reachable body
(
xml.NewDecoder(limited)/json.NewDecoder(limited)), and for GCS avoid themap[string]interface{}unmarshal of untrusted JSON where practical.
Secondary hardening note (not a remote vulnerability)
libgobuster/helpers.go → ParseCommaSeparatedInt expands numeric ranges into a set
with for i := fromI; i <= toI; i++ { ret.Add(i) }. A range such as
--status-codes 0-2147483647 would attempt billions of map inserts (local self-DoS).
This is operator-supplied input, not attacker-controlled, so it is a robustness nit
rather than a security issue — mentioned only for completeness. A cap on total range
size would address it.
Suggested disclosure handling
The maintainer may reasonably treat this as a normal bug fix rather than an embargoed
advisory given the client-side, operator-chosen-target threat model. Recommended: fix
in the open via a PR/issue referencing this report; request a CVE only if the project
prefers to track it formally (comparable client-side cases such as Ollama's gzip-bomb
CVE-2024-12886 did receive IDs).
Prepared as a responsible-disclosure draft. All testing was performed locally against a self-hosted mock server; no third-party systems were touched.
Source: OJ/gobuster