GHSA-VP88-798V-H67F
Vulnerability from github – Published: 2026-10-27 16:08 – Updated: 2026-10-27 16:09In rustls 0.23.44 and 0.23.45, application data that a client buffers before the handshake completes is sent to the server even when the server rejects Encrypted Client Hello (ECH).
When the server rejects ECH, the client completes the handshake with the outer ClientHello and authenticates the server against the ECH config's public_name. At the end of the handshake, start_traffic() encrypts and queues any plaintext buffered by ClientConnection::writer(). It does this before checking for EchStatus::Rejected and queueing the ech_required alert.
The application then receives Error::PeerIncompatible(ServerRejectedEncryptedClientHello), as expected. However, the client is still expected to call write_tls() to send the fatal alert, and that call also sends the encrypted application data. The server decrypts the data and only then receives the alert.
The ordering bug is older than 0.23.44. Before 0.23.44, the client checked the server certificate against the inner server name rather than public_name, so a correctly configured server failed certificate verification first and no data was sent. 0.23.44 fixed that check, which makes the ordinary rejection path reachable.
Impact
Plaintext meant for the inner server, the one named in the encrypted ClientHello, goes to the client-facing server identified by public_name. That server is properly authenticated, so this is not an attack by an unauthenticated party. However this violates RFC 9849, which says the following:
- Section 6.1.6: if the handshake and authentication succeed after ECH rejection, the client "MUST ... abort the connection with an "ech_required" alert before sending any application data to the server."
- Section 6.1.7: the certificate is checked only against
public_name, and the connection "MUST NOT" be reported as successful to the application. rustls does report failure correctly. The problem is only that data is sent before the alert.
This affects only clients that meet all of these conditions:
- They use
EchMode::Enable. GREASE ECH is not affected. - They write application data before the handshake completes.
- They connect to a server that rejects their ECH config, for example because the config is stale. This is the same case where the server returns retry configs.
The usual client response to rejection is to retry with the server's retry configs. In that case, the data has already gone to the outer server.
{
"affected": [
{
"package": {
"ecosystem": "crates.io",
"name": "rustls"
},
"ranges": [
{
"events": [
{
"introduced": "0.23.44"
},
{
"fixed": "0.23.46"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "crates.io",
"name": "rustls"
},
"ranges": [
{
"events": [
{
"introduced": "0.24.0-dev.1"
},
{
"fixed": "0.24.0-dev.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-200"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-27T16:08:59Z",
"nvd_published_at": null,
"severity": "LOW"
},
"details": "In rustls 0.23.44 and 0.23.45, application data that a client buffers before the handshake completes is sent to the server even when the server rejects Encrypted Client Hello (ECH).\n\nWhen the server rejects ECH, the client completes the handshake with the outer ClientHello and authenticates the server against the ECH config\u0027s `public_name`. At the end of the handshake, `start_traffic()` encrypts and queues any plaintext buffered by `ClientConnection::writer()`. It does this *before* checking for `EchStatus::Rejected` and queueing the `ech_required` alert.\n\nThe application then receives `Error::PeerIncompatible(ServerRejectedEncryptedClientHello)`, as expected. However, the client is still expected to call `write_tls()` to send the fatal alert, and that call also sends the encrypted application data. The server decrypts the data and only then receives the alert.\n\nThe ordering bug is older than 0.23.44. Before 0.23.44, the client checked the server certificate against the inner server name rather than `public_name`, so a correctly configured server failed certificate verification first and no data was sent. 0.23.44 fixed that check, which makes the ordinary rejection path reachable.\n\n# Impact\n\nPlaintext meant for the inner server, the one named in the encrypted ClientHello, goes to the client-facing server identified by `public_name`. That server is properly authenticated, so this is not an attack by an unauthenticated party. However this violates [RFC 9849](https://www.rfc-editor.org/info/rfc9849), which says the following:\n\n- Section 6.1.6: if the handshake and authentication succeed after ECH rejection, the client \"MUST ... abort the connection with an \"ech_required\" alert **before sending any application data to the server.**\"\n- Section 6.1.7: the certificate is checked only against `public_name`, and the connection \"MUST NOT\" be reported as successful to the application. rustls does report failure correctly. The problem is only that data is sent before the alert.\n\nThis affects only clients that meet all of these conditions:\n\n- They use `EchMode::Enable`. GREASE ECH is not affected.\n- They write application data before the handshake completes.\n- They connect to a server that rejects their ECH config, for example because the config is stale. This is the same case where the server returns retry configs.\n\nThe usual client response to rejection is to retry with the server\u0027s retry configs. In that case, the data has already gone to the outer server.",
"id": "GHSA-vp88-798v-h67f",
"modified": "2026-10-27T16:09:00Z",
"published": "2026-10-27T16:08:59Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/rustls/rustls/security/advisories/GHSA-vp88-798v-h67f"
},
{
"type": "WEB",
"url": "https://github.com/rustls/rustls/pull/3300"
},
{
"type": "WEB",
"url": "https://github.com/rustls/rustls/pull/3354"
},
{
"type": "WEB",
"url": "https://github.com/rustls/rustls/commit/2bd5c0cefabb2c5fd4100383a736d59f9671b9b4"
},
{
"type": "WEB",
"url": "https://github.com/rustls/rustls/commit/4de14a508fbfa2273203ec17d9dfdd0fbd29e8d1"
},
{
"type": "WEB",
"url": "https://github.com/rustls/rustls/commit/7dcbe4c1ae12185237d22e9197583329630a2014"
},
{
"type": "PACKAGE",
"url": "https://github.com/rustls/rustls"
},
{
"type": "WEB",
"url": "https://github.com/rustls/rustls/releases/tag/v/0.23.46"
},
{
"type": "WEB",
"url": "https://rustsec.org/advisories/RUSTSEC-2026-0347.html"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "rustls: ECH rejection can expose buffered application data to authenticated outer server before `ech_required` alert"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
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.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
How durations are calculated
Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.
Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.