Skip to content

bug: buffered header merge overwrites backend Set-Cookie headers when Traefik writes a sticky-session cookie (breaks OIDC logins) #57

Description

@antoinemichea

Summary

The plugin's buffered responseWriter merges its buffered headers into the real writer by assignment when WriteHeader is called. When Traefik's sticky-session load balancer writes its cookie before proxying the request (the writer is not ready yet, so the cookie lands in the buffer), the merge overwrites the Set-Cookie headers of the backend response. Only the sticky cookie reaches the client; every cookie set by the backend on that response is silently dropped.

Why this matters (use case)

Any OAuth/OIDC login flow behind a sticky Traefik service breaks: the backend's redirect to the IdP sets a state cookie (e.g. WildFly/Elytron's OAuth_Token_Request_State) on the same response where Traefik sets its sticky cookie. The state cookie is dropped, so the IdP callback fails — WildFly logs WARN [org.wildfly.security.http.oidc] No state cookie and returns a plain 400 Bad Request.

The bug only fires on requests where Traefik actually writes the sticky cookie (i.e. the client doesn't send it yet): fresh browsers, private windows, expired session cookies. Returning users with the sticky cookie already set are unaffected, which makes it look like random user-specific failures and very hard to diagnose. We hit this in production on ~80 environments (Traefik v3.6.11, plugin v1.3.0).

Current behavior (main.go, responseWriter.WriteHeader)

headers := r.responseWriter.Header()
for header, value := range r.headers {
	headers[header] = value
}

Timeline of a request through sablier (dynamic) → service with sticky cookie:

  1. The plugin wraps the writer; ready=false, so Header() returns the buffered map.
  2. Traefik's load balancer calls WriteStickyCookie before ServeHTTP on the proxy → the sticky cookie is Added to the buffered map.
  3. The proxy sends the request to the backend; the httptrace WroteHeaders/WroteRequest callbacks flip ready=true.
  4. The proxy copies the backend response headers (including its Set-Cookie) onto the real writer (Header() now returns it).
  5. The proxy calls WriteHeader → the merge loop assigns headers["Set-Cookie"] = ["<sticky>"], replacing the backend's cookies.

Reproduction

Traefik (tested v3.6.11) + this plugin as a local plugin, file provider: a router with the sablier middleware (dynamic strategy, Sablier answering X-Sablier-Session-Status: ready) in front of a service with sticky.cookie enabled, backend answering 302 + Set-Cookie: TESTSTATE=abc123.

  • curl -sD - http://proxy/ (no sticky cookie sent) → response contains only the sticky cookie; TESTSTATE is gone.
  • Same request with the sticky cookie already set → TESTSTATE passes through (Traefik doesn't rewrite the sticky cookie).
  • Same router without the sablier middleware → both cookies present (Traefik core is fine, the drop happens in the plugin's merge).

Proposed fix

Set-Cookie is multi-valued — each value is an independent cookie, so merging by assignment is always lossy. Append the buffered values instead of overwriting for this header (other headers keep the current overwrite semantics):

if header == "Set-Cookie" {
	headers[header] = append(headers[header], value...)
} else {
	headers[header] = value
}

PR with the fix + regression tests (unit-level on responseWriter and end-to-end through ServeHTTP with an httptrace-firing next handler): #58

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions