rustls 0.23.46 fixes data leak after ECH rejection
rustls 0.23.44 and 0.23.45 can send buffered application data to the outer server after that server rejects ECH. If your client uses EchMode::Enable, update to rustls 0.23.46 or later.

On October 14, 2026, RustSec issued RUSTSEC-2026-0347 (alias GHSA-vp88-798v-h67f) for the rustls TLS library. After a server rejects Encrypted Client Hello (ECH), rustls 0.23.44 and 0.23.45 can send buffered application data to the outer, client-facing server. If your client uses EchMode::Enable and writes data before the handshake finishes, update to rustls 0.23.46 or later.
What changed in rustls
The advisory title is "Buffered application data sent to outer server after ECH rejection". It sits in the crypto-failure category and has a CVSS 3.1 score of 2.3 LOW (vector CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N).
With ECH, a client sends two ClientHellos. The inner one names the real server and is encrypted. The outer one names a client-facing server, given by the ECH config's public_name. If the server rejects ECH, the rustls client finishes the handshake with the outer ClientHello and checks the server certificate against public_name.
The bug is in the order of the last steps. At the end of that handshake, rustls encrypted and queued any application data the app had already buffered through ClientConnection::writer(). Only after that did it queue the ech_required alert. The app does get the correct error, Error::PeerIncompatible(ServerRejectedEncryptedClientHello). But the next write_tls() call, which is needed to send the alert, also sends the queued application data. Plaintext meant for the inner server goes to the server named by public_name.
This breaks RFC 9849 section 6.1.6, which says the client must abort with ech_required "before sending any application data to the server".
The advisory says the ordering bug itself is older. Before 0.23.44, the client checked the server certificate against the inner server name, not public_name. Certificate checks failed first, so no data was ever sent. Once 0.23.44 changed the check to public_name, the old ordering bug started to leak data.
The fix is rustls PR #3300, "client: fail on ECH rejection before encrypting application data". It is merged into main. In the fixed code, the client fails on ECH rejection before it encrypts any buffered data.
| rustls version | Status in RUSTSEC-2026-0347 |
|---|---|
| below 0.23.44 | Unaffected (<0.23.44) |
| 0.23.44, 0.23.45 | Affected |
| 0.23.46 and later | Patched (>=0.23.46) |
RustSec published three more rustls advisories on the same day: RUSTSEC-2026-0346, RUSTSEC-2026-0348 and RUSTSEC-2026-0349. Check each one against your own configuration. This article covers only 0347.
Who is affected and why it matters
The advisory lists three conditions. All three must be true:
- The client uses
EchMode::Enable. GREASE ECH is not affected. - The client writes application data before the handshake completes, for example an HTTP request written to
ClientConnection::writer()right after the connection is created. - The client connects to a server that rejects its ECH config, "for example, because it is stale".
The third condition is not rare. ECH configs are usually fetched from DNS (HTTPS records) and can change. A client with a cached, outdated config will see rejections in normal use, with no attacker involved.
The impact is limited. The server that receives the data has a valid certificate for public_name, so this is "not exploitable by an unauthenticated party", in the advisory's words. That is why it gets a high privileges-required rating and a low confidentiality score. Still, ECH exists to hide which inner server a client wants to reach. Here, an affected client can send the request itself (paths, headers, cookies, tokens) to the outer operator, such as a CDN front-end. That operator would normally only see the outer handshake.
Servers do not have this problem. Neither do clients that leave ECH off, use GREASE only, or wait for the handshake to finish before writing.
What to do now
These steps use Cargo and cargo-audit. They apply to any project that depends on rustls 0.23.x, directly or through crates such as HTTP clients and async TLS wrappers.
- Check whether rustls is in your build, and which version:
cargo tree -i rustls
- If the output shows 0.23.44 or 0.23.45, move the lockfile to a patched version. A semver-compatible update is enough:
cargo update -p rustls
# or pin the exact patched release
cargo update -p rustls --precise 0.23.46
- If you depend on rustls directly, raise the minimum version in your Cargo manifest so the old releases cannot come back:
[dependencies]
rustls = "0.23.46"
- Run the advisory check locally and in CI. After the database update, it reports RUSTSEC-2026-0347 for any lockfile that still has an affected version:
cargo install cargo-audit --locked
cargo audit
-
If you cannot upgrade right away, avoid the affected path. Either do not write application data until the handshake has finished (for example, wait until
is_handshaking()returns false), or use GREASE ECH instead ofEchMode::Enablefor now. -
Refresh ECH configs often. This does not fix the bug, but a fresh config makes rejections, and so this code path, less likely.
Caveats
The score and affected range come from the advisory itself. The "Unaffected" entry covers only this bug: releases below 0.23.44 avoided it because certificate verification failed first. It says nothing about the other rustls advisories issued the same day. Before you decide which version to pin, check RUSTSEC-2026-0346, -0348 and -0349 too.
Get the wire in your inbox
Every new signal, straight from the generator. No noise, unsubscribe anytime.


