Servers, proxies and network services
HAProxy: CVE-2026-110353 truncated HTTP/2 PING frame
HAProxy HTTP/2 PING check
HAProxy's HTTP/2 code does not treat a truncated PING frame as a short read (CVE-2026-110353). A PING frame cannot be used until it is complete, but its presence still wakes the connection up, so the worker thread never goes back to idle. A remote client with no login can open a connection, keep a partial PING frame pending and hold a thread at 100% CPU for as long as the connection stays open or until timeout http-request runs out. The vendor rates the impact as moderate, because it depends on how many of your connections look like this.
| CVE | CVE-2026-110353 (denial of service in h2c_handle_ping in src/mux_h2.c), published 22 October 2026 |
| Severity | High 7.5 |
| Affected | 3.1.x (end of life), 3.2.x before 3.2.26, 3.3.x before 3.3.17, 3.4.x before 3.4.7. 3.0 and older are outside this CVE's range (but see the Red Hat advisories below: Red Hat shipped an advisory for its 3.0 build) |
| Fixed | 3.4.7, 3.3.17 and 3.2.26, all released 14 October 2026 |
| Vendor workaround | Set timeout http-request to a low value such as 1 to 5 seconds. It limits CPU consumption, it does not fix the bug |
| Second flaw | The same releases fix a CPU-exhaustion flaw in the HTTP/1 parser (a chunked request with a chunk-size line longer than the buffer). The vendor does not state its affected range, so this tool does not rule out any HTTP-mode HAProxy because of it |
The CVE text and the vendor disagree, and this tool follows the vendor. The CVE description says "3.2 before 3.4.6". HAProxy's own release notes say the short-read handling was fixed in 2.5 but PING was missed. They say it only became a problem in 3.1, when the wake-up logic changed, and that the fixes shipped in 3.4.7, 3.3.17 and 3.2.26. So 3.4.6 is not fixed, whatever the CVE description implies.
Quick start
-
Download the script. The repository is mirrored on GitHub and Codeberg; pick a base URL.
# BASE from GitHub: https://raw.githubusercontent.com/PrivacySafe/security-patches/refs/heads/main # BASE from Codeberg: https://codeberg.org/PrivacySafe/security-patches/raw/branch/main # curl -fLO "$BASE/haproxy/haproxy-check.sh" curl -fLO https://raw.githubusercontent.com/PrivacySafe/security-patches/refs/heads/main/haproxy/haproxy-check.sh -
Read it before running it, then make it executable.
less haproxy-check.sh chmod +x haproxy-check.sh -
Check this host. The script finds HAProxy on
PATH, in the usual locations and from running processes, and reads the configuration files the running process was started with. To check a specific binary, pass its path../haproxy-check.sh check ./haproxy-check.sh check /opt/haproxy/sbin/haproxy
Commands
| Command | What it does |
|---|---|
check [BINARY ...] (default) |
Reads the version from haproxy -v, applies the vendor's ranges, checks whether the running processes use a binary that was replaced on disk, and reads the configuration for HTTP/2 reachability and the timeout http-request value |
mitigate [--timeout N] [--disable-h2-upgrade] [--dry-run] [--reload] [--force] |
Changes the host (root). Applies the vendor's workaround: adds timeout http-request 5s (or N, 1 to 60) to every defaults section that has no such line, and never changes a value you set. --disable-h2-upgrade also adds option disable-h2-upgrade (PrivacySafe recommendation). It runs haproxy -c before and after, copies every file first, and puts the originals back at once if HAProxy rejects the result. Nothing is reloaded unless you add --reload (a graceful reload). It does nothing on a version outside the affected range (3.1 to 3.4 before the fixed releases) unless you add --force |
rollback [--dry-run] [--reload] |
Puts back every file mitigate changed |
version |
Prints the script version |
Environment: HAPROXY_CFG (space-separated files or directories) replaces configuration discovery. STATE_DIR (default /var/lib/psf-security-patches/haproxy) holds the backups and the manifest that rollback reads.
Exit codes: 0 nothing exposed, 1 an affected version is exposed, its configuration could not be read, or it is on the unfixable 3.1 branch, 2 bad arguments.
Commands that change the host need root. Run them with sudo from an admin account, not from a root login, so the change is recorded against a person. The script stops and prints the exact command if you forget:
sudo ./haproxy-check.sh mitigate --dry-run
sudo ./haproxy-check.sh mitigate
sudo ./haproxy-check.sh rollback
How HTTP/2 becomes reachable, which decides exposure
- TLS listeners offer HTTP/2 only when the
bindline hasalpn h2.proto h2on abindline forces HTTP/2. - Clear-text HTTP listeners accept the HTTP/2 preface by default and upgrade the connection, unless the frontend has
option disable-h2-upgrade. So a plainbind :80inmode httpis reachable over HTTP/2 even though no configuration line saysh2. This is easy to miss, and the script does not treat "no explicit h2" as safe. alpn h2on a backendserverline is HAProxy talking HTTP/2 to your servers. It does not expose the frontend, and the script ignores it.- Comments are ignored.
mode tcponly: the HTTP/2 multiplexer is not in use, so the script reports no exposure.
The exposure test is a text search of the configuration, not a parse of the live configuration. If HAProxy is built from many included files or generated at runtime, check haproxy -c -f output and the running configuration yourself.
How to read the result
- Affected and exposed: upgrade to 3.2.26, 3.3.17 or 3.4.7 or later, then reload.
- Affected but no HTTP/2 path found: exit
0. Ifmode httpis in use, the script still says the HTTP/1 flaw fixed in the same releases may be reachable, so upgrade anyway. - 3.1.x: always exit
1. The branch is end of life: move to a supported branch. - Development builds (
3.5-dev) cannot be judged by version. The script asks you to check that the build includes the "properly handle short reads on PING frames" change. - Distribution packages backport fixes without changing the version. The script reads the package changelog for a mention of the CVE.
- A reload keeps the old binary running in some setups. If a process runs a binary that was replaced on disk, the script says so.
Fixing it
Upgrade to 3.2.26, 3.3.17 or 3.4.7 or later and reload. Until then, apply the vendor's workaround for the PING flaw with ./haproxy-check.sh mitigate --dry-run and then ./haproxy-check.sh mitigate --reload. It limits CPU use while a partial PING frame is pending. It does not fix the bug, and it does not change a timeout http-request you already set. If you do not need clear-text HTTP/2, adding option disable-h2-upgrade to HTTP frontends and removing h2 from alpn lists takes the multiplexer out of reach. That last step is PrivacySafe's recommendation.
Verified on HAProxy 3.1.0, 3.2.0 and 3.4.0 built from source, each with a clear-text listener. The check flagged each one (3.1 as end of life) and haproxy -c accepted both added lines. A half-sent request stayed open for the full six seconds before the change and got a 408 after two seconds with timeout http-request 2s. rollback restored the file exactly and the old behavior.
Sources
Before you run it
- The fix. Update to 3.4.7, 3.3.17 or 3.2.26. Branch 3.1 is end of life: move to a supported branch.
- Two sources disagree and the script follows the vendor. The CVE description says "3.2 before 3.4.6", but HAProxy's release notes say the bug was introduced in 3.1 and that 3.4.6 is not fixed. HAProxy 3.0 and older are outside this CVE's range.
- Development builds are not judged. For a 3.5 development build, check the commit yourself.
- The impact is a denial of service. A client holding a partial PING frame can keep a worker thread at full CPU until the connection ends or
timeout http-requestexpires. There is no hunt. - It also checks running docker and podman containers, unless you name a directory or set
SCAN_CONTAINERS=0. A read-only probe inside each running docker and podman container captures the output ofhaproxy -vand the configuration files (withpasswordvalues andauth user:passcredentials redacted inside the container before they are copied) into a private temporary directory. The script then runs once per container, in isolation, against that copy, so one container's configuration never changes another's result. Each container is judged by its own configuration only. The copies are removed on exit. A path shown as.../docker_NAME/...is inside the container NAME. It is skipped when you name a binary or directory yourself.
Red Hat advisories
Red Hat's advisory list (security advisories released 2026-04-01 to 2026-11-03) names the CVEs below. A Red Hat build is judged by its RPM release, not by the upstream version number, because Red Hat backports fixes: the advisory names the release that fixes it. A CVE with no advisory in the list has none for that period, which is not the same as not affected. The current state is on Red Hat's CVE database.
The only advisory is for a 3.0 build, although HAProxy gives 3.1 as the first affected line: check says so for a 3.0 build that is a Red Hat one.
| Advisory | Released | Severity | Package and stream | CVEs on this page |
|---|---|---|---|---|
| RHSA-2026:83844 | 2026-10-16 | Important | haproxy-3.0.30-0.1.hum1 (Hummingbird) |
CVE-2026-110353 |
The Security Patches software and this documentation are dedicated to the public domain under CC0 1.0 Universal. Other pages on privacysafe.app/docs are CC BY-SA 4.0 unless the page says otherwise.