forward_auth: copy_headers does not strip client-supplied identity headers (Fixes GHSA-7r4p-vjf4-gxv4)#7545
Merged
francislavoie merged 1 commit intocaddyserver:masterfrom Mar 4, 2026
Conversation
…lied headers When using copy_headers in a forward_auth block, client-supplied headers with the same names were not being removed before being forwarded to the backend. This happens because PR caddyserver#6608 added a MatchNot guard that skips the Set operation when the auth service does not return a given header. That guard prevents setting headers to empty strings, which is the correct behavior, but it also means a client can send X-User-Id: admin in their request and if the auth service validates the token without returning X-User-Id, Caddy skips the Set and the client value passes through unchanged to the backend. The fix adds an unconditional delete route for each copy_headers entry, placed just before the existing conditional set route. The delete always runs regardless of what the auth service returns. The conditional set still only runs when the auth service provides that header. The end result is: - Client-supplied headers are always removed - When the auth service returns the header, the backend gets that value - When the auth service does not return the header, the backend sees nothing Existing behavior is unchanged for any deployment where the auth service returns all of the configured copy_headers entries. Fixes GHSA-7r4p-vjf4-gxv4
francislavoie
approved these changes
Mar 4, 2026
copy_headers does not strip client-supplied identity headers (Fixes GHSA-7r4p-vjf4-gxv4)
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is the problem?
The
forward_authdirective withcopy_headersdoes not remove client-supplied headers before forwarding the request to the backend.An attacker with any valid authentication token can send a request that includes forged identity headers like
X-User-Id: adminorX-User-Role: superadmin. If the auth service validates the token and returns 200 OK without including those headers in its response, Caddy skips the Set operation (the MatchNot guard added by PR #6608 fires) and never removes the original client-supplied values. The backend receives the attacker's values and may grant elevated access.This is a regression introduced by PR #6608 (November 2024). All stable releases from v2.10.0 onward are affected.
Common patterns that trigger this:
claims as response headers (the backend decodes the JWT itself)
What does this change?
For each entry in
copy_headers, a new unconditional delete route is added immediately before the existing conditional set route. The delete runs always. The set still only runs when the auth service includes that header in its response.Result:
Existing behavior is unchanged for deployments where the auth service returns all configured
copy_headersentries.Changes
modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go: core fixforward_auth_authelia.caddyfiletestandforward_auth_rename_headers.caddyfiletestto reflect new delete routesin expected JSON output
forward_auth_copy_headers_strip.caddyfiletestfor the security scenarioforwardauth_test.gowith two integration tests: one that confirms injected headers are stripped when the auth service does not return them, and one that confirms the auth service value wins when it does return themFact-Check Report
1. Core Fix —
caddyfile.gotois the correct variable:to = http.CanonicalHeaderKey(headersToCopy[from])is the destination header name. ForA>1,to = "1". Clients inject the destination header (e.g.,1: admin), so we must deleteto.MatcherSetsRaw→omitemptymeans no"match"key → route always executes.[delete, conditional-set]per header. Delete fires first (removes any client-supplied value), then set fires only if the auth service returned that header. When auth does not return it: delete removed the forge, set is skipped → backend sees absent header.goodResponseHandlerwhich hasStatusCode: []int{2}. The reverseproxy code (reverseproxy.go:1047) skips any handler whoserh.Matchdoes not match the status code. So 4xx/5xx from auth bypasses the delete entirely — client sees the exact error response.2. JSON Fixture Structure — Confirmed Mechanism
Resolved the
"handle"before"match"ordering: routes embedded inside handlers go throughcaddyconfig.JSONModuleObject, which doesjson.Marshal → unmarshal to map[string]any → add "handler" key → json.Marshal(map). Go'sjson.Marshalon maps sorts keys alphabetically."handle"(h) <"match"(m), so inner routes have"handle"first. This is verified by the pre-existing upstream fixtures that pass Caddy's CI.forward_auth_rename_headers— 5 delete routes, destination names ("1","B","3","D","5"), 12-tab depth.forward_auth_authelia— 4 delete routes, destination names (Remote-Email/Groups/Name/User sorted), 16-tab depth (4 more due to subroute wrapper fromapp.example.comhostname matcher).forward_auth_copy_headers_strip(new) — deletes X-User-Id and X-User-Role, 12-tab depth, mirrors rename_headers structure exactly."match"key (omitempty).3. Integration Test —
forwardauth_test.gocaddytestimports_ "github.com/caddyserver/caddy/v2/modules/standard"which chains tocaddyhttp/standard/imports.gowhich includes_ ".../reverseproxy/forwardauth". Theforward_authdirective is registered.admin localhost:2999andhttp_port 9080are the standard Caddy test ports used by all other integration tests.t.Runanti-pattern: Sequential assertions using the outer*testing.T.tc.t.Fatalfcorrectly fails the outer test.sync.Mutexprotectslast.AssertResponsewaits for a complete HTTP round-trip; by the time it returns, the backend handler has already written tolast.goodResponseHandlerskipped → client sees 401 as-isTestForwardAuthCopyHeadersAuthResponseWins: auth returns headers → delete removes forge, set copies auth value → backend gets auth value4. No New Security Bugs
Fixes GHSA-7r4p-vjf4-gxv4