CVE-2025-0239
Overview
Background
- Alt-Svc (Alternative Services)
- an HTTP mechanism (
Alt-Svcheader) that lets an origin advertise that its content is also reachable over a different host, port, or protocol, allowing the client to transparently connect elsewhere. - ALPN (Application-Layer Protocol Negotiation)
- a TLS extension in which the client and server agree, during the handshake, on the application protocol (e.g.
h2,http/1.1) to speak over the connection. - NPN token
- the negotiated/next-protocol identifier that
nsHttpConnectiontracks; the value the transaction expected (mConnInfo->GetNPNToken()) is compared against the value the peer actually negotiated (GetNegotiatedNPN). - `DontReuse`
- an
nsHttpConnectionmethod that marks a connection as non-poolable so it is torn down instead of being handed to another transaction.
Root Cause Analysis
When an Alt-Svc route caused a transaction to be coalesced onto or routed over a TLS connection, nsHttpConnection::HandshakeDoneInternal accepted the completed handshake without confirming that the protocol actually negotiated over the wire matched the protocol the transaction’s connection info (mConnInfo) required. Because the code retrieved negotiatedNPN but never compared it against the transaction’s expected NPN token, a redirect or alternative-service pointing at an insecure or mismatched endpoint could satisfy the handshake even though the certificate/protocol validation appropriate for the intended origin was effectively bypassed. The invariant violated is that a connection may only carry a transaction whose negotiated ALPN/NPN identity equals the identity the transaction was authorized for.
The fix reads the transaction’s expected token via mConnInfo->GetNPNToken(), and when it is non-empty and does not equal negotiatedNPN, it calls DontReuse and closes the transaction with NS_ERROR_NET_RESET, forcing a fresh, properly validated connection instead of trusting the mismatched one.
NS_ERROR_NET_RESET whenever negotiatedNPN differs from mConnInfo->GetNPNToken().Attack Path
- Advertise an alternative service
A server the victim contacts returns an
Alt-Svcheader steering the client toward an attacker-influenced or insecure alternative endpoint. - Coalesce onto a mismatched connection Firefox routes the transaction over a connection whose negotiated ALPN/NPN identity does not correspond to the origin the transaction actually intended to reach.
- Handshake accepted unchecked
nsHttpConnection::HandshakeDoneInternalfinishes the handshake and readsnegotiatedNPNbut, pre-patch, never compares it to the expectedmConnInfo->GetNPNToken(), so the mismatch goes undetected. - Serve content over the wrong identity The transaction proceeds on a connection whose certificate/protocol validation does not match the intended secure origin, letting the attacker’s insecure endpoint effectively impersonate or downgrade the connection.
Impact Assessment
Files Changed
netwerk/protocol/http/nsHttpConnection.cppnetwerk/test/unit/head_channels.js
Audit Directions
- Negotiated-vs-expected identity checksAudit other handshake and connection-completion paths (e.g. HTTP/2 coalescing, WebSocket, proxied connections) to ensure the negotiated ALPN/NPN and certificate identity are always compared against what the transaction actually requested.
- Alt-Svc and redirect routingReview all code that reroutes transactions via
Alt-Svc, connection coalescing, or redirects to confirm the target endpoint’s security properties are revalidated rather than inherited from the original origin. - Retrieved-but-unused security valuesLook for places that fetch a security attribute (like
GetNegotiatedNPN) into a local variable but never act on it, since such dead reads often signal a missing validation check.
Patch
diff --git a/netwerk/protocol/http/nsHttpConnection.cpp b/netwerk/protocol/http/nsHttpConnection.cpp
index e5d391604f3..8e9e8031a2d 100644
--- a/netwerk/protocol/http/nsHttpConnection.cpp
+++ b/netwerk/protocol/http/nsHttpConnection.cpp
@@ -2419,6 +2419,17 @@ void nsHttpConnection::HandshakeDoneInternal() {
DebugOnly<nsresult> rvDebug = securityInfo->GetNegotiatedNPN(negotiatedNPN);
MOZ_ASSERT(NS_SUCCEEDED(rvDebug));
+ nsAutoCString transactionNPN;
+ transactionNPN = mConnInfo->GetNPNToken();
+ LOG(("negotiatedNPN: %s - transactionNPN: %s", negotiatedNPN.get(),
+ transactionNPN.get()));
+ if (!transactionNPN.IsEmpty() && negotiatedNPN != transactionNPN) {
+ LOG(("Resetting connection due to mismatched NPN token"));
+ DontReuse();
+ mTransaction->Close(NS_ERROR_NET_RESET);
+ return;
+ }
+
bool earlyDataAccepted = false;
if (mTlsHandshaker->EarlyDataUsed()) {
// Check if early data has been accepted.
diff --git a/netwerk/test/unit/head_channels.js b/netwerk/test/unit/head_channels.js
index 94a20b0a3d5..c4c28fa4e06 100644
--- a/netwerk/test/unit/head_channels.js
+++ b/netwerk/test/unit/head_channels.js
@@ -377,6 +377,7 @@ async function asyncStartTLSTestServer(
Services.env.set("LD_LIBRARY_PATH", greBinDir.path + ":/data/local/xpcb");
Services.env.set("MOZ_TLS_SERVER_DEBUG_LEVEL", "3");
Services.env.set("MOZ_TLS_SERVER_CALLBACK_PORT", CALLBACK_PORT);
+ Services.env.set("MOZ_TLS_ECH_ALPN_FLAG", "1");
let httpServer = new HttpServer();
let serverReady = new Promise(resolve => {
Regression Test / PoC
diff --git a/netwerk/test/unit/head_channels.js b/netwerk/test/unit/head_channels.js
index 94a20b0a3d5..c4c28fa4e06 100644
--- a/netwerk/test/unit/head_channels.js
+++ b/netwerk/test/unit/head_channels.js
@@ -377,6 +377,7 @@ async function asyncStartTLSTestServer(
Services.env.set("LD_LIBRARY_PATH", greBinDir.path + ":/data/local/xpcb");
Services.env.set("MOZ_TLS_SERVER_DEBUG_LEVEL", "3");
Services.env.set("MOZ_TLS_SERVER_CALLBACK_PORT", CALLBACK_PORT);
+ Services.env.set("MOZ_TLS_ECH_ALPN_FLAG", "1");
let httpServer = new HttpServer();
let serverReady = new Promise(resolve => {