CVE-2026-89422 Overview
CVE-2026-89422 is a certificate validation bypass in the Erlang/OTP ssl application affecting TLS 1.3 client connections. A malicious peer can return a pre_shared_key extension in the ServerHello that the client never offered. The client then completes the handshake without validating the server's certificate, allowing full server impersonation without possession of any private key, certificate, or prior session state. The flaw is classified under CWE-322: Key Exchange without Entity Authentication. Default client configurations are affected; clients restricted to TLS 1.2 are not.
Critical Impact
A network-positioned attacker can impersonate any TLS 1.3 server to an Erlang/OTP client, bypassing certificate path validation, verify_fun, hostname verification, partial_chain, CRL checking, and OCSP stapling.
Affected Products
- Erlang/OTP ssl from version 9.5 before 11.2.12.13 (OTP 22.2 through OTP 27.3.4.17)
- Erlang/OTP ssl before 11.6.0.6 (OTP 28.5.0.6 and earlier in the 28.x branch)
- Erlang/OTP ssl before 11.7.7 (OTP 29.1.0 and earlier in the 29.x branch)
Discovery Timeline
- 2026-09-22 - CVE-2026-89422 published to NVD
- 2026-09-22 - Last updated in NVD database
Technical Details for CVE-2026-89422
Vulnerability Analysis
The vulnerability sits in the TLS 1.3 client state machine. When the client receives a ServerHello, tls_client_connection_1_3:handle_server_hello/2 extracts the pre_shared_key extension and passes it to tls_gen_connection_1_3:handle_resumption/2. That function sets resumption = true on the mere presence of the extension, without verifying that the client actually offered a PSK.
Once resumption is set, maybe_resumption/1 routes execution directly to wait_finished, skipping every certificate-handling state. The client therefore never validates the server's certificate chain, hostname, revocation status, or OCSP staple. The call to ssl:connect returns {ok, Socket} against a peer that holds no certificate and no private key.
Root Cause
Two related defects combine to produce the bypass. First, handle_resumption/2 trusts the server-selected extension unconditionally. Second, tls_handshake_1_3:get_pre_shared_key/4 falls back to the all-zero "no PSK" value when session tickets are not configured, keying the handshake with the ordinary non-PSK schedule. The attacker's own ephemeral key material is sufficient to complete the transcript, because both sides derive the same zero PSK.
Attack Vector
An attacker with a network-layer man-in-the-middle position, or one who controls a hostname the Erlang client is instructed to connect to, answers the client's TLS 1.3 ClientHello with a ServerHello that includes an unsolicited pre_shared_key extension. No certificate or CertificateVerify message is required. The Erlang client accepts the handshake and treats the attacker as the authenticated server.
%% Security patch — lib/ssl/src/tls_handshake_1_3.erl
%% Reject unsolicited TLS-1.3 pre_shared_key in client
%% Server initiates a full handshake
get_pre_shared_key(_, _, HKDFAlgo, undefined) ->
{ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))};
%% Session resumption not configured, i.e. we did not offer any PSK, so the
%% server's selected_identity cannot be within the range we supplied.
%% RFC 8446 Section 4.2.11: abort with an "illegal_parameter" alert instead of
%% silently falling back to the (zero) "no PSK" value, which would skip server
%% authentication.
get_pre_shared_key(undefined, _, _, ServerPSK) ->
{error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})};
get_pre_shared_key(_, undefined, _, ServerPSK) ->
{error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})};
Source: GitHub OTP Commit 98c66c8. The patch replaces the silent zero-PSK fallback with a fatal illegal_parameter alert whenever the server selects a PSK the client never offered.
Detection Methods for CVE-2026-89422
Indicators of Compromise
- Successful ssl:connect/2,3,4 return values from Erlang clients paired with TLS 1.3 sessions in which no Certificate or CertificateVerify handshake messages were exchanged.
- ServerHello messages containing a pre_shared_key extension in flows where the corresponding ClientHello did not include a pre_shared_key extension.
- Unexpected TLS 1.3 sessions negotiated to attacker-controlled IP addresses from Erlang-based services (RabbitMQ, ejabberd, CouchDB, VerneMQ, and similar).
Detection Strategies
- Perform packet capture on outbound TLS 1.3 flows from Erlang/OTP hosts and flag handshakes where the server response omits Certificate and CertificateVerify while the client did not advertise session tickets.
- Inventory installed Erlang/OTP releases and enumerate the bundled ssl application version with application:get_key(ssl, vsn). to identify vulnerable deployments.
- Correlate application logs with TLS handshake telemetry to identify clients that completed handshakes to untrusted or unpinned hostnames.
Monitoring Recommendations
- Enable TLS handshake logging in Erlang via the log_level and log_alertssl:connect options to capture handshake state transitions.
- Ingest network TLS metadata into a security data lake to baseline expected TLS 1.3 handshake shapes per Erlang service.
- Alert on connections where an Erlang client presents no client certificate and the server response contains a pre_shared_key extension without a corresponding client offer.
How to Mitigate CVE-2026-89422
Immediate Actions Required
- Upgrade Erlang/OTP to a fixed release: OTP 27.3.4.18 (ssl 11.2.12.13), OTP 28.5.0.7 (ssl 11.6.0.6), or OTP 29.1.1 (ssl 11.7.7).
- Restart all Erlang applications and BEAM VMs after upgrading so the patched ssl application is loaded into running nodes.
- Audit dependencies for embedded or pinned Erlang/OTP runtimes in RabbitMQ, ejabberd, CouchDB, and other BEAM-based products, and apply their vendor updates.
Patch Information
The fix is delivered across four upstream commits in the erlang/otp repository and coordinated in GitHub Security Advisory GHSA-rgxr-4g4w-j875. Relevant commits: 21b8a1b, 98c66c8, afec515, and fd1d9d0. The patched code path now returns a fatal illegal_parameter alert when the server selects a PSK the client did not offer, and only invokes handle_resumption after validating that the client advertised a matching PSK identity. Additional detail is available in the CNA CVE-2026-89422 advisory and the OSV CVE-2026-89422 report.
Workarounds
- Restrict Erlang TLS clients to TLS 1.2 by passing {versions, ['tlsv1.2']} to ssl:connect, since TLS 1.2 code paths are not affected.
- Pin server certificates or public keys through a custom verify_fun and reject connections that complete without an observed certificate chain.
- Terminate outbound TLS at a hardened proxy (for example, HAProxy or Envoy) that performs certificate validation before the Erlang runtime is involved.
# Verify installed ssl application version from the shell
erl -noshell -eval 'application:load(ssl), {ok, V} = application:get_key(ssl, vsn), io:format("~s~n", [V]), halt().'
# Temporary mitigation: force TLS 1.2 in client calls
# ssl:connect(Host, Port, [{versions, ['tlsv1.2']}, {verify, verify_peer}, {cacertfile, "/etc/ssl/certs/ca-certificates.crt"}]).
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.