Medium CVSS 4 firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
4
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionWhen using Alt-Svc, ALPN did not properly validate certificates when the original server is redirecting to an insecure site.
ComponentNetworking
Bug ClassLogic Error
Tracker1929156
Fix commit0a86e065709b (firefox) +12/-0
CWECWE-295
CVSS vectorCVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedPaul Gerste
Disclosed2025-01-07

Background

Alt-Svc (Alternative Services)
an HTTP mechanism (Alt-Svc header) 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 nsHttpConnection tracks; the value the transaction expected (mConnInfo->GetNPNToken()) is compared against the value the peer actually negotiated (GetNegotiatedNPN).
`DontReuse`
an nsHttpConnection method 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.

Key insight
The core mistake was completing the TLS handshake path without verifying that the negotiated ALPN/NPN token matched the token the transaction expected, so an Alt-Svc redirect to an insecure endpoint could be silently accepted; the fix closes the connection with NS_ERROR_NET_RESET whenever negotiatedNPN differs from mConnInfo->GetNPNToken().

Attack Path

  1. Advertise an alternative service A server the victim contacts returns an Alt-Svc header steering the client toward an attacker-influenced or insecure alternative endpoint.
  2. 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.
  3. Handshake accepted unchecked nsHttpConnection::HandshakeDoneInternal finishes the handshake and reads negotiatedNPN but, pre-patch, never compares it to the expected mConnInfo->GetNPNToken(), so the mismatch goes undetected.
  4. 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

An attacker positioned to influence Alt-Svc responses can cause a transaction to be served over a connection whose ALPN/NPN identity was not properly validated against the intended origin, undermining the certificate/transport guarantees expected for that origin. This occurs in the browser’s networking stack (the socket/HTTP connection layer of the parent/network process) and requires that the original server use Alt-Svc and redirect to an insecure or mismatched site. The result is a moderate-severity confusion of connection identity rather than direct memory corruption.

Files Changed

  • netwerk/protocol/http/nsHttpConnection.cpp
  • netwerk/test/unit/head_channels.js

Audit Directions

  • Negotiated-vs-expected identity checks
    Audit 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 routing
    Review 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 values
    Look 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.
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 => {
Loading diff…

Regression Test / PoC

shipped with the fix
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 => {
Loading diff…