GitHub Advisoryadvisory · Go
GHSA-p6hg-qh38-555r Traefik's errors middleware forwards Authorization and Cookie headers to separate error page service
MEDIUM
Official page ↗
- Aliases
- CVE-2026-41181
- Ecosystem
- Go
- Packages
- github.com/traefik/traefik/v2, github.com/traefik/traefik/v3
- Severity
- Moderate
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N
- Weaknesses
- CWE-201
- Reviewed
- 2026-05-04
Affected packages
• Go github.com/traefik/traefik/v2: < 2.11.44 → fixed in 2.11.44
• Go github.com/traefik/traefik/v3: < 3.6.15 → fixed in 3.6.15
• Go github.com/traefik/traefik/v3: >= 3.7.0-rc.0, < 3.7.0-rc.3 → fixed in 3.7.0-rc.3
Description
Summary
There is a medium severity information disclosure vulnerability in Traefik's `errors` (custom error pages) middleware. When the backend returns a response matching the configured status range, the middleware forwards the original request's complete header set, including `Authorization`, `Cookie`, and other authentication material, to the separate error page service rather than only the minimal context needed to render the error page. This behavior is undocumented: the documentation states only that `Host` is forwarded by default, so operators are not warned that sensitive credentials are shared across service boundaries. Deployments using the `errors` middleware with a distinct error page service may inadvertently expose end-user credentials to infrastructure that was not intended to receive them.
Patches
• https://github.com/traefik/traefik/releases/tag/v2.11.44
• https://github.com/traefik/traefik/releases/tag/v3.6.15
• https://github.com/traefik/traefik/releases/tag/v3.7.0-rc.3
For more information
If there are any questions or comments about this advisory, please open an issue.
Original Description
Description
Traefik v3.6.13's supported HTTP `errors` middleware discloses sensitive request headers to the configured error page service when the original backend response matches the configured status range and the middleware takes its default header-forwarding path. In the reproduced configuration, the business router `audit-customerrors@docker` pointed to backend service `audit-backend`, attached middleware `audit-leak@docker`, and the middleware was configured with `errors.status=500-599`, `errors.service=audit-error`, and `errors.query=/collect`. A request to the business route caused the backend to return `500`, after which Traefik created a secondary request to the error service and copied the original `Authorization` and `Cookie` headers into that cross-service request.
This is a normal feature path on an ordinary HTTP route. It does not depend on `api.insecure`, the dashboard, pprof, or a debug-only mode. The confidentiality boundary that breaks here is the service boundary between the original backend chain and the separate error page service: credentials that were only meant for the original backend are automatically delivered to another service.
The root cause is in `pkg/middlewares/customerrors/custom_errors.go:151-160`:
```go
if len(c.forwardNginxHeaders) > 0 {
utils.CopyHeaders(pageReq.Header, c.forwardNginxHeaders)
pageReq.Header.Set("X-Code", strconv.Itoa(code))
pageReq.Header.Set("X-Format", req.Header.Get("Accept"))
pageReq.Header.Set("X-Original-Uri", req.URL.RequestURI())
} else {
utils.CopyHeaders(pageReq.Header, req.Header)
}
```
Unless the `NginxHeaders` branch is explicitly used, the middleware copies the entire original request header map into the error page request. The documentation at `docs/content/reference/routing-configuration/http/middlewares/errorpages.md:103-107` only states that `Host` is forwarded by default, so operators are not warned that `Authorization`, `Cookie`, and other authentication material are forwarded as well.
Steps To Reproduce
1. Deploy Traefik v3.6.13 with a normal business route that uses the supported `errors` middleware and points `errors.service` to a distinct service. The attached PoC uses `BASE_URL = "http://127.0.0.1:28080"`, `API_BASE_URL = "http://127.0.0.1:28180"`, `ROUTER_PATH = "/audit-customerrors"`, `AUTHORIZATION = "Bearer audit-secret-token"`, and `COOKIE = "sessionid=audit-cookie; theme=dark"`.
2. Start the two attached helper services `customerrors_backend.py` and `customerrors_error.py`. The backend listens on port `8000` and always returns `500`. The error service listens on port `8000` and returns the request method, path, and received headers as JSON. The PoC starts them with the router and middleware labels below so that the business request is handled by the backend, while the error page is fetched from the separate error service:
```text
traefik.http.routers.audit-customerrors.rule=PathPrefix(`/audit-customerrors`)
traefik.http.routers.audit-customerrors.entrypoints=web
traefik.http.routers.audit-customerrors.priority=100
traefik.http.routers.audit-customerrors.service=audit-backend
traefik.http.routers.audit-customerrors.middlewares=audit-leak
traefik.http.services.audit-backend.loadbalancer.server.port=8000
traefik.http.middlewares.audit-leak.errors.status=500-599
traefik.http.middlewares.audit-leak.errors.service=audit-error
traefik.http.middlewares.audit-leak.errors.query=/collect
```
3. Confirm that Traefik has loaded the route and middleware. The attached `customerrors_router.json` shows that `audit-customerrors@docker` uses middleware `audit-leak@docker`, and the attached `customerrors_middleware.json` shows that the middleware is enabled with `status` `500-599`, `service` `audit-error`, and `query` `/collect`.
4. Send a request containing sensitive credentials through the business route. The manual reproduction used the following request, and the automated PoC sends the same header values:
```bash
curl -i \
-H 'Authorization: Bearer audit-secret-token' \
-H 'Cookie: sessionid=audit-cookie; theme=dark' \
http://127.0.0.1:28080/audit-customerrors
```
5. Observe that the backend returns `500`, Traefik internally requests `/collect` from the error service, and the error service receives the original `Authorization` and `Cookie` headers. The attached `manual_curl_customerrors.txt` response shows the leaked headers directly, and the attached `poc_customerrors_header_leak.output.txt` execution log shows the same result from the automated PoC.
Recommendations
The default behavior should forward only the minimal context needed to render an error page instead of copying the full original header set with `utils.CopyHeaders(pageReq.Header, req.Header)`. At minimum, Traefik should strip `Authorization`, `Proxy-Authorization`, `Cookie`, `Set-Cookie`, and common custom authentication headers such as `X-Api-Key` before issuing the error page request. If operators truly need additional headers, that behavior should be opt-in through an explicit allowlist rather than the default. The documentation should also describe the current behavior and warn that routing an error page to a separate service can otherwise disclose end-user credentials across service boundaries.
PoC
The main PoC attachment is `poc_customerrors_header_leak.py`.
```python
import json
import os
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
TARGET = "traefik customErrors sensitive header leak"
BASE_URL = "http://127.0.0.1:28080"
API_BASE_URL = "http://127.0.0.1:28180"
TRAEFIK_CONTAINER = "traefik-openclaw"
NETWORK = ""
DOCKER_IMAGE = "python:3.12-alpine"
BACKEND_CONTAINER = "traefik-audit-backend"
ERROR_CONTAINER = "traefik-audit-error"
ROUTER_NAME = "audit-customerrors"
ROUTER_PATH = "/audit-customerrors"
AUTHORIZATION = "Bearer audit-secret-token"
COOKIE = "sessionid=audit-cookie; theme=dark"
TIMEOUT_SECONDS = 10
ROUTER_WAIT_SECONDS = 20
EVIDENCE_DIR = Path(__file__).resolve().parent
BACKEND_SCRIPT = EVIDENCE_DIR / "customerrors_backend.py"
ERROR_SCRIPT = EVIDENCE_DIR / "customerrors_error.py"
def run_command(command):
print(f"$ {' '.join(command)}")
completed = subprocess.run(command, capture_output=True, text=True, check=True)
stdout = completed.stdout.strip()
stderr = completed.stderr.strip()
if stdout:
print(stdout)
if stderr:
print(stderr)
return stdout
def remove_container(name):
subprocess.run(["docker", "rm", "-f", name], capture_output=True, text=True)
def detect_network():
if NETWORK:
return NETWORK
output = run_command(
["docker", "inspect", TRAEFIK_CONTAINER, "--format", "{{json .NetworkSettings.Networks}}"]
)
networks = json.…
Related
Alias · CVE (1)
- CVE-2026-41181 Traefik is an HTTP reverse proxy and load balancer. vulnerability
Weakness · CWE (1)
- CWE-201 Insertion of Sensitive Information Into Sent Data weakness
References
- https://github.com/traefik/traefik/security/advisories/GHSA-p6hg-qh38-555r web
- https://nvd.nist.gov/vuln/detail/CVE-2026-41181 advisory
- https://github.com/traefik/traefik package
- https://github.com/traefik/traefik/releases/tag/v2.11.44 web
- https://github.com/traefik/traefik/releases/tag/v3.6.15 web
- https://github.com/traefik/traefik/releases/tag/v3.7.0-rc.3 web