Hi,
HAProxy 3.3.14 was released on 2026/08/27. It added 96 new commits
after version 3.3.13.
This fixes a lot of issues, and a sizeable share of them were found
and reported with the help of AI-based tools/agents (Claude, OpenAI, and
a couple of other AI-assisted researchers), which seems to be an efficient way
to dig up this kind of corner case, so thanks to everyone involved for the
reports. A few areas received more attention than others and are worth
upgrading for:
- CVE-2026-78120: a TLS session established on one "bind" of an HAProxy
process could be resumed on a different "bind" of that same process,
regardless of the certificate or the client-certificate verification policy
configured on it.
With stateful resumption (the shared session cache), this worked between
any two binds of the process.
With stateless resumption (tickets), it worked between any binds sharing
the same tls-ticket-keys, even on a different process. By default,
tls-ticket-keys are generated randomly per bind, so this doesn't apply
unless the same keys are explicitly shared across binds.
In practice this meant a session/ticket obtained on a bind with no
client-certificate requirements could be resumed on another bind enforcing
strict mutual TLS, without the client ever presenting a certificate on that
second connection.
Configurations that gate access by checking whether a certificate was
actually used, rather than trusting the bind's handshake verification
result, were not affected: ssl_c_used correctly reflects certificate usage
on the current connection, whether resumed or not. As a mitigation without
upgrading, this can be enforced in TCP mode:
tcp-request content reject if ! { ssl_c_used }
or in HTTP mode:
http-request deny if ! { ssl_c_used }
Alternatively, the stateful session cache can be disabled entirely, leaving
stateless resumption -- whose tls-ticket-keys are randomly generated per
bind by default -- as the only resumption mechanism:
global
tune.ssl.cachesize 0
Note that, independently of this issue, it is good practice to inspect
the certificate presented by the client, as explained on the wiki:
https://github.com/haproxy/wiki/wiki/mTLS-configuration-in-HAProxy#inspecting-the-client-certificate
Three fixes now scope session/ticket resumption to the actual
certificate, verification policy and crt-list filter in use.
- tune.ssl.lifetime wasn't being enforced across TLS 1.3 session renewals
(including on AWS-LC), letting a resumed/renewed session outlive the
configured limit.
- CVE-2026-78121: qpack_get_varint(), used to decode QPACK-encoded HTTP/3
header fields, could shift a 64-bit value by 63 bits or more when parsing a
varint with an excessive number of continuation bytes, which is undefined
behavior in C. Impact is low: this was filed as BUG/MINOR since no
exploitable consequence beyond the UB itself was identified on the
compilers/platforms tested. The shift count is now explicitly bounded
before use.
- the "sink"/log forwarding code received 4 fixes: a "log tcp@..." line in
the global section could crash at boot because its implicit forwarding
server was left uninitialized; that same kind of implicit server, in an
unrelated gap, also kept max-reuse disabled, causing a new TCP connection
to be opened for every single log line instead of reusing it; a lock was
needlessly held around ring_dispatch_messages(), which could deadlock; and
a new implicit ring could get created for each copy of a logger, which was
a waste of memory and could confuse "show events".
- when a message's body was shorter than its announced Content-Length, the H1
mux still reused the connection and the H2 mux still marked the message as
complete, silently forwarding a truncated message and risking confusion
with the next request on a reused connection; the H1 mux now closes the
connection instead, and the H2 mux no longer marks the message as complete.
- the cache code got 3 fixes: an entry could be released while still
referenced under the cache read lock, a secondary entry could be retained
without its primary one when detaching a row, and a request with a
duplicated Origin or Referer header could match a secondary key computed
from only the first occurrence, so such requests now bypass the cache
instead of risking an unpredictable match.
- a handful of correctness fixes: an absolute-form request-target with no
path but a query string (e.g. "http://host?token=...") had its query string
swallowed into the authority, corrupting the request on the wire; rewriting
a server-side cookie ("cookie ... rewrite" or "prefix") could silently fail
to expand and still corrupt the byte following it in the response; a TCP
payload filter could lose buffered data if the producer shut its connection
while the filter still had pending data to flush; Lua's Channel:send()
could resend the same prefix instead of the remainder after a partial
write, duplicating data and desynchronizing the stream; HPACK could
truncate H2 request methods/schemes longer than 127 bytes, which in
practice mostly caused shared backend connections to be aborted with a
protocol error under "http-reuse always"; and the "aes_gcm_dec" converter
and the JWE AES-GCM key-unwrap path accepted a truncated authentication tag
instead of requiring the full 16 bytes, which is now enforced.
- "clear counters all" used to segfault the next request on any server,
since it zeroed the shared per-thread-group counters pointers that every
hot path dereferences without a NULL check; this also fixes "clear
counters all" not actually resetting cumulative counters as documented.
Thanks to Alexander Stephan for the patch.
On top of this, a new has_ctl() converter was added to detect control
characters in a sample.
Upgrading is recommended for everyone running 3.3, especially those relying on
client certificate authentication.
Please find the usual URLs below :
Site index : https://www.haproxy.org/
Documentation : https://docs.haproxy.org/
Wiki : https://github.com/haproxy/wiki/wiki
Discourse : https://discourse.haproxy.org/
Slack channel : https://slack.haproxy.org/
Issue tracker : https://github.com/haproxy/haproxy/issues
Q&A from devs : https://github.com/orgs/haproxy/discussions
Sources : https://www.haproxy.org/download/3.3/src/
Git repository : https://git.haproxy.org/git/haproxy-3.3.git/
Git Web browsing : https://git.haproxy.org/?p=haproxy-3.3.git
Changelog : https://www.haproxy.org/download/3.3/src/CHANGELOG
Dataplane API :
https://github.com/haproxytech/dataplaneapi/releases/latest
Pending bugs : https://www.haproxy.org/l/pending-bugs
Reviewed bugs : https://www.haproxy.org/l/reviewed-bugs
Code reports : https://www.haproxy.org/l/code-reports
Latest builds : https://www.haproxy.org/l/dev-packages
---
Complete changelog :
Alexander Stephan (1):
BUG/MEDIUM: counters: preserve shared.tg pointer on 'clear counters all'
Amaury Denoyelle (4):
BUG/MINOR: server: check strdup return value on server ID
BUG/MINOR: proxy: fix default-server leak on post-parsing cleanup
BUG/MEDIUM: quic: prevent out-of-bound read on wrapping CRYPTO content
BUG/MINOR: quic: drop multiple Retry on same connection
Aurelien DARRAGON (5):
MINOR: log/tools: fix ambiguous comments for some log encoding helpers
BUG/MEDIUM: log: always reserve room for trailing 0 when using CBOR
encoding helpers
BUG/MEDIUM: lua: resume Channel:send() from the unsent part of the string
BUG/MEDIUM: hlua_fcn: ensure systematic bref cleanup for patref list
iterator
BUG/MEDIUM: stats-file: fix shm-stats-file recover when all process slots
are full
Austin Kauffman (1):
BUG/MINOR: fcgi-app: allow explicit filter declaration with
non-cache/non-compression filters
Christopher Faulet (6):
BUG/MEDIUM: filter: Disable auto-close on channel during TCP payload
filtering
BUG/MINOR: config: Check buffer pool creation for failures
BUG/MINOR: hlua: Properly enable/disable receives for TCP applets
BUG/MINOR: tcpcheck: Don't release ruleset when parsing 'spop-check'
ruleset
BUG/MEDIUM: tools: make string encoding possible to fail instead of
truncating
BUG/MINOR: flt-http-comp: Don't read next block to detect end of data
Frederic Lecaille (2):
BUG/MINOR: server: fix off-by-one error when parsing and copying source
port range
BUG/MINOR: qpack: missing shift count check in qpack_get_varint() (UB)
Kirill Furman (1):
BUG/MINOR: log: fix double-free error when error in parse_loger occurs
Mani Goyal (1):
BUG/MEDIUM: http: fix authority parsing for absolute-form URI with empty
path
Manu Nicolas (1):
BUG/MINOR: resolvers: accept fields at the response boundary
Miroslav Zagorac (1):
BUG/MINOR: ot: removed dead code in flt_ot_parse_cfg_str()
Olivier Houchard (12):
BUG/MINOR: cli: use the current argument to parse the FD spec in "show fd"
BUG/MEDIUM: spoe: clear the applet pointer when the applet fails to start
BUG/MINOR: mux-fcgi: don't call fcgi_strm_destroy() on a NULL stream
BUG/MEDIUM: http-ana: don't crash on "keep-query" in a response redirect
BUG/MEDIUM: stick-tables: use the same bucket for string keys with a NUL
BUG/MEDIUM: sock: bound the recvmsg() length when receiving old sockets
BUG/MEDIUM: http-ana: check the cookie rewrite result before moving the
offsets
BUG/MINOR: mux-fcgi: sanitize the STDERR records before logging them
BUG/MEDIUM: bwlim: fix a stick-table entry leak in shared mode
BUG/MEDIUM: mux-fcgi: check the room left before appending the index
BUG/MEDIUM: mux-h1: close the connection on a short content-length
MEDIUM: mux-h2: don't report EOM on a short content-length message
Remi Tricot-Le Breton (8):
BUG/MINOR: jwt: don't take an extra reference on the certificate public
key
BUG/MEDIUM: ssl: require a full-length AEAD tag when decrypting with
AES-GCM
BUG/MINOR: conn: Do not check 'sess_el' list on frontend connections in
__trace_enabled
BUG/MINOR: ssl: Fix leak of X509_NAME in traces
BUG/MINOR: ssl: release the previous client cert reference at depth > 0
BUG/MEDIUM: ssl: Fix unprotected 'ssl_sock_choose_sni_ctx' calls
BUG/MINOR: jwt: Missing 'jwt_tokenize' return value check
MINOR: jwt: Improve 'jwt_tokenize' function
Rémi Tricot-Le Breton (2):
BUG/MEDIUM: cache: retain the primary or secondary entry only when
detaching its row
BUG/MEDIUM: cache: do not release an entry under the cache read lock
William Lallemand (22):
BUG/MINOR: ech: propagate error from load_echkeys()
BUG/MINOR: ech: reject an ECH store with no usable private key
BUG/MEDIUM: acme: don't delete a NULL token from the map
BUG/MINOR: ssl: reject server certificate names containing a NUL byte
BUG/MINOR: acme: restrict the permissions of the generated account key
DOC: config: clarify req.ssl_sni
BUG/MINOR: ssl: reject an embedded NUL in the ssl_*_dn(entry) fetches
BUG/MINOR: ssl: reject an embedded NUL in the full-DN ssl_*_dn() fetches
BUG/MINOR: payload: fix handshake length off-by-4 in ssl_hello_sni/alpn
BUG/MINOR: spoe: check snprintf() return value in
spoe_set_var/spoe_unset_var
BUG/MEDIUM: ssl: enforce tune.ssl.lifetime across TLS1.3 session renewals
BUG/MINOR: ssl: apply tune.ssl.lifetime to TLS1.3 sessions on
BoringSSL/AWS-LC
BUILD: ssl: disable the TLS1.3 session timeout clamp on wolfSSL
BUG/MEDIUM: ssl: isolate TLS session resumption per X509 server
certificate
BUG/MINOR: ssl: isolate TLS session resumption per crt-list filter
BUG/MEDIUM: ssl: isolate TLS session resumption per authentication policy
BUILD: ssl: avoid a potential null-dereference warning on OpenSSL 1.0.2
BUG/MINOR: ssl/cli: fix frontend-not-found detection in 'show ssl sni -f'
BUG/MINOR: ech: fix label at end of compound statement
Revert "BUG/MINOR: jwt: Missing 'jwt_tokenize' return value check"
BUG/MINOR: admin: haproxy-reload use explicit socat address type
BUG/MINOR: acme: NULL check on my_strndup()
Willy Tarreau (28):
BUG/MEDIUM: hpack: encode long methods and schemes using the long form
BUG/MINOR: stats-file: reject tgid 0 when preloading shm objects
BUG/MINOR: hlua: use a local buffer to format the socket addresses
BUG/MINOR: connection: reserve the whole CRC32C TLV before saving its
pointer
BUG/MEDIUM: session: don't release a reversed connection twice on error
BUG/MINOR: lb-chash: bound the walk when the saved cursor changed tree
BUG/MINOR: debug: only dump the trace once in __BUG_ON_ONCE()
BUG/MINOR: mux-h2: strip the userinfo when deriving :authority for a
server
BUG/MINOR: quic: avoid a division by zero in the BBR pacing interval
BUG/MEDIUM: sink: pre-initialize the implicit log forwarding server
BUG/MINOR: http-fetch: fix smp_fetch_hdr_ip()'s handling of brackets for
IPv6
BUG/MINOR: http-fetch: make http_first_req() check for HTTP first
BUG/MINOR: http-act: set-status() must check the response message, not
the request
BUG/MINOR: tools: fix memory leak in env_expand() error path
BUG/MINOR: auth: free user groups on error paths in userlist_postinit()
BUG/MINOR: cfgcond: make KQUEUE check for GTUNE_USE_KQUEUE not
GTUNE_USE_EPOLL
BUG/MINOR: mqtt: connack parser returns MQTT_NEED_MORE_DATA on unknown
property
BUG/MINOR: mqtt: connect parser uses wrong bit field for
TOPIC_ALIAS_MAXIMUM
BUG/MINOR: mqtt: connack parser uses wrong bit for
SUBSCRIPTION_IDENTIFIERS_AVAILABLE
BUG/MINOR: mqtt: fix PUBLISH flags validation that want all bits to be set
BUG/MINOR: mux-h2: harden h2_dump_h2s_info() against potentially null
h2s->sd
BUG/MEDIUM: connection: fix an infinite loop in the fc_pp_tlv() fetch
BUG/MEDIUM: sink: do not hold the sft lock around ring_dispatch_messages()
MINOR: sample: add new converter has_ctl() to detect control characters
REGTESTS: converters: use connection: close in the has_ctl() test
BUG/MEDIUM: cache: ignore cache on redundant origin/referer
BUG/MEDIUM: sink: initialize the settings of the implicit log server
BUG/MEDIUM: sink: do not create one implicit ring per logger copy
scientiamobile (1):
BUG/MINOR: wurfl: fix memory leak of information list and patch strings
at deinit
---
--
William Lallemand