GitHub Security Advisory: GHSA-7r4p-vjf4-gxv4
Package: github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy (GO)
Severity: HIGH (CVSS 8.1)
CWE: CWE-287: Improper Authentication, CWE-345: Insufficient Verification of Data Authenticity
Vulnerable versions: >= 2.10.0, < 2.11.2
Patched in: 2.11.2

Summary: Caddy forward_auth copy_headers Does Not Strip Client-Supplied Headers, Allowing Identity Injection and Privilege Escalation

Details:
## Summary

Caddy's `forward_auth` directive with `copy_headers` generates conditional header-set operations that only fire when the upstream auth service includes the named header in its response. No delete or remove operation is generated for the original client-supplied request header with the same name.

When an auth service returns `200 OK` without one of the configured `copy_headers` headers, the client-supplied header passes through unchanged to the backend. Any requester holding a valid authentication token can inject arbitrary values for trusted identity headers, resulting in privilege escalation.

This is a regression introduced by PR #6608 in November 2024. All stable releases from v2.10.0 onward are affected.

---

## Scope Argument

This is a bug in the source code of this repository, not a misconfiguration.

The operator uses `forward_auth` with `copy_headers` exactly as documented. The documentation contains no warning that client-supplied headers with the same names as `copy_headers` entries must also be stripped manually. The `forward_auth` directive is a security primitive whose stated purpose is to gate backend access behind an external auth service. A user of this directive reasonably expects that the backend cannot receive a client-controlled value for a header listed in `copy_headers`.

The bug is traceable to a specific commit: PR #6608 (merged November 4, 2024), which added a `MatchNot` guard to skip the `Set` operation when the auth response header is absent. This change, while fixing a legitimate UX issue (headers being set to empty strings), removed the incidental protection that the previous unconditional `Set` provided. Before PR #6608, setting a header to an empty/unresolved placeholder overwrote the attacker-supplied value. After PR #6608, the attacker's value survives.

The fix is a single-line code change in `modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go`.

---

## Affected Versions

| Version | Vulnerable |
|---|---|
| <= v2.9.x | No (old code overwrote client value with empty placeholder) |
| v2.10.0 (April 18, 2025) | Yes — first stable release containing PR #6608 |
| v2.10.1 | Yes |
| v2.10.2 | Yes |
| v2.11.0 | Yes |
| v2.11.1 (February 23, 2026, current) | Yes — unpatched |

**Package:** `github.com/caddyserver/caddy/v2`
**Affected file:** `modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go`

---

## Root Cause

The `parseCaddyfile` function builds one route per `copy_headers` entry. Each route uses a `MatchNot` guard and a `Set` operation:

```go
// from modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go (v2.11.1, identical in v2.10.x)
copyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{
    MatcherSetsRaw: []caddy.ModuleMap{{
        "not": h.JSON(caddyhttp.MatchNot{MatcherSetsRaw: []caddy.ModuleMap{{
            "vars": h.JSON(caddyhttp.VarsMatcher{
                "{" + placeholderName + "}": []string{""},
            }),
        }}}),
    }},
    HandlersRaw: []json.RawMessag