Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in QUIC
DescriptionInappropriate implementation in QUIC
ComponentQUIC
Bug ClassLogic Error
Tracker495793059
Fix commit11c8b6b1891e (chromium/src) +47/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
TEST_P
net/quic/quic_chromium_client_session_test.cc
modified

Files Changed

  • net/quic/quic_chromium_client_session.cc
  • net/quic/quic_chromium_client_session_peer.cc
  • net/quic/quic_chromium_client_session_peer.h
  • net/quic/quic_chromium_client_session_test.cc
From 11c8b6b1891e205cf1c7fa5fd8896f28d6c99452 Mon Sep 17 00:00:00 2001
From: Yoshisato Yanagisawa <[email protected]>
Date: Thu, 18 Jun 2026 01:46:42 -0700
Subject: [PATCH] Bugfix: This CL fixes a bug reported in crbug.com/495793059

[analysis & reasoning]
https://docs.google.com/document/d/13ReqU8edwhUsVs3Vh8hvh81yUhzfaNVKqVAeIGefSv4

Bug: 495793059
Change-Id: I4398d24f45edbca8f4624e3e8cdffacfce496c08
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7953930
Commit-Queue: Yoshisato Yanagisawa <[email protected]>
Reviewed-by: Kenichi Ishibashi <[email protected]>
Reviewed-by: Nidhi Jaju <[email protected]>
Reviewed-by: David Schinazi <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1648872}
---

diff --git a/net/quic/quic_chromium_client_session.cc b/net/quic/quic_chromium_client_session.cc
index f2c7262..25e7b5c 100644
--- a/net/quic/quic_chromium_client_session.cc
+++ b/net/quic/quic_chromium_client_session.cc
@@ -4105,6 +4105,7 @@
   // confirmed if the session is not created on the default network.
   if (migrate_session_on_network_change_v2_ &&
       default_network_ != handles::kInvalidNetworkHandle &&
+      session_key_.proxy_chain().is_direct() &&
       GetCurrentNetwork() != default_network_) {
     current_migration_cause_ = ON_MIGRATE_BACK_TO_DEFAULT_NETWORK;
     StartMigrateBackToDefaultNetworkTimer(
diff --git a/net/quic/quic_chromium_client_session_peer.cc b/net/quic/quic_chromium_client_session_peer.cc
index 04d6853..c765dbb 100644
--- a/net/quic/quic_chromium_client_session_peer.cc
+++ b/net/quic/quic_chromium_client_session_peer.cc
@@ -68,4 +68,16 @@
   session->default_network_ = network;
 }
 
+// static
+bool QuicChromiumClientSessionPeer::IsMigrateBackToDefaultNetworkTimerRunning(
+    QuicChromiumClientSession* session) {
+  return session->migrate_back_to_default_timer_.IsRunning();
+}
+
+// static
+void QuicChromiumClientSessionPeer::OnCryptoHandshakeComplete(
+    QuicChromiumClientSession* session) {
+  session->OnCryptoHandshakeComplete();
+}
+
 }  // namespace net::test
diff --git a/net/quic/quic_chromium_client_session_peer.h b/net/quic/quic_chromium_client_session_peer.h
index 68ca551..4c12204c 100644
--- a/net/quic/quic_chromium_client_session_peer.h
+++ b/net/quic/quic_chromium_client_session_peer.h
@@ -39,6 +39,11 @@
 
   static void SetDefaultNetwork(QuicChromiumClientSession* session,
                                 handles::NetworkHandle network);
+
+  static bool IsMigrateBackToDefaultNetworkTimerRunning(
+      QuicChromiumClientSession* session);
+
+  static void OnCryptoHandshakeComplete(QuicChromiumClientSession* session);
 };
 
 }  // namespace test
diff --git a/net/quic/quic_chromium_client_session_test.cc b/net/quic/quic_chromium_client_session_test.cc
index 4d4ee587..c51b358 100644
--- a/net/quic/quic_chromium_client_session_test.cc
+++ b/net/quic/quic_chromium_client_session_test.cc
@@ -2930,6 +2930,35 @@
       MIGRATION_STATUS_ALREADY_MIGRATED, 1);
 }
 
+TEST_P(QuicChromiumClientSessionTest, NoMigrationForProxiedSessionOnHandshake) {
+  ProxyChain proxy_chain(ProxyServer::SCHEME_HTTPS,
+                         HostPortPair("proxy.example.com", 443));
+  session_key_ = QuicSessionKey(
+      kServerHostname, kServerPort, PRIVACY_MODE_DISABLED, proxy_chain,
+      SessionUsage::kDestination, SocketTag(), NetworkAnonymizationKey(),
+      SecureDnsPolicy::kAllow, /*require_dns_https_alpn=*/false,
+      /*disable_cert_verification_network_fetches=*/false,
+      handles::kInvalidNetworkHandle);
+
+  // Initialize with kInvalidNetworkHandle so the test socket gets bound to it
+  // (simulating QuicProxyDatagramClientSocket's behavior).
+  default_network_ = handles::kInvalidNetworkHandle;
+  Initialize(/*migrate_session_on_network_change_v2=*/true);
+
+  // Now set the session's default_network_ to a valid handle, simulating the
+  // physical user's network. This creates the exact mismatch condition for the
+  // bug.
+  QuicChromiumClientSessionPeer::SetDefaultNetwork(
+      session_.get(), handles::kInvalidNetworkHandle + 1);
+
+  QuicChromiumClientSessionPeer::OnCryptoHandshakeComplete(session_.get());
+
+  // The timer MUST NOT be running for a proxied session.
+  EXPECT_FALSE(
+      QuicChromiumClientSessionPeer::IsMigrateBackToDefaultNetworkTimerRunning(
+          session_.get()));
+}
+
 TEST_P(QuicChromiumClientSessionTest, GoingAwaySessionDoesNotKeepAlive) {
   MockQuicData quic_data(version_);
   quic_data.AddWrite(SYNCHRONOUS, client_maker_.MakeInitialSettingsPacket(1));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/net/quic/quic_chromium_client_session_test.cc b/net/quic/quic_chromium_client_session_test.cc
index 4d4ee587..c51b358 100644
--- a/net/quic/quic_chromium_client_session_test.cc
+++ b/net/quic/quic_chromium_client_session_test.cc
@@ -2930,6 +2930,35 @@
       MIGRATION_STATUS_ALREADY_MIGRATED, 1);
 }
 
+TEST_P(QuicChromiumClientSessionTest, NoMigrationForProxiedSessionOnHandshake) {
+  ProxyChain proxy_chain(ProxyServer::SCHEME_HTTPS,
+                         HostPortPair("proxy.example.com", 443));
+  session_key_ = QuicSessionKey(
+      kServerHostname, kServerPort, PRIVACY_MODE_DISABLED, proxy_chain,
+      SessionUsage::kDestination, SocketTag(), NetworkAnonymizationKey(),
+      SecureDnsPolicy::kAllow, /*require_dns_https_alpn=*/false,
+      /*disable_cert_verification_network_fetches=*/false,
+      handles::kInvalidNetworkHandle);
+
+  // Initialize with kInvalidNetworkHandle so the test socket gets bound to it
+  // (simulating QuicProxyDatagramClientSocket's behavior).
+  default_network_ = handles::kInvalidNetworkHandle;
+  Initialize(/*migrate_session_on_network_change_v2=*/true);
+
+  // Now set the session's default_network_ to a valid handle, simulating the
+  // physical user's network. This creates the exact mismatch condition for the
+  // bug.
+  QuicChromiumClientSessionPeer::SetDefaultNetwork(
+      session_.get(), handles::kInvalidNetworkHandle + 1);
+
+  QuicChromiumClientSessionPeer::OnCryptoHandshakeComplete(session_.get());
+
+  // The timer MUST NOT be running for a proxied session.
+  EXPECT_FALSE(
+      QuicChromiumClientSessionPeer::IsMigrateBackToDefaultNetworkTimerRunning(
+          session_.get()));
+}
+
 TEST_P(QuicChromiumClientSessionTest, GoingAwaySessionDoesNotKeepAlive) {
   MockQuicData quic_data(version_);
   quic_data.AddWrite(SYNCHRONOUS, client_maker_.MakeInitialSettingsPacket(1));
Loading diff…

Original Bug Report

reported by [email protected]

Potential MASQUE proxy bypass and IP leak via QUIC connection migration

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: Proxied QUIC sessions incorrectly trigger connection migration back to the default network upon handshake completion due to a network handle mismatch. This sends a direct UDP probe to the proxy, leaking the client’s real IP address. A non-fatal DUMP_WILL_BE_CHECK then allows the session to fully migrate to an unencapsulated socket, bypassing the proxy tunnel entirely.

Affected files:

  • net/quic/quic_chromium_client_session.cc
  • net/quic/quic_session_pool.cc

Estimated timestamp from git blame: 2025-04-18

Description

There is a potential privacy leak and encapsulation bypass in Chromium’s QUIC client when using proxied sessions, such as those used for MASQUE or IP Protection. The vulnerability stems from a logical flaw where proxied sessions incorrectly trigger connection migration, leading them to send unencapsulated UDP packets directly to the proxy server.

In a multi-hop proxy configuration (like IP Protection), this flaw causes the client’s real IP address to be exposed to the second-hop proxy, violating the split-knowledge privacy model. Furthermore, the session fully migrates out of the proxy tunnel, sending all subsequent traffic unencapsulated.

Root Cause Analysis

The vulnerability is a chain of logic bugs across session initialization, migration triggering, and non-fatal assertions:

  1. Network Handle Mismatch during Initialization: When QuicSessionPool::CreateSessionOnProxyStream creates a proxied session, it intentionally sets the network handle to handles::kInvalidNetworkHandle to prevent migration. However, in QuicSessionPool::CreateSessionHelper (net/quic/quic_session_pool.cc:2091), the pool passes its globally tracked valid default_network_ to the QuicChromiumClientSession constructor instead of the explicitly invalid network parameter.

  2. Spurious Migration Trigger: Upon successful completion of the cryptographic handshake, QuicChromiumClientSession::OnCryptoHandshakeComplete() checks if it needs to migrate back to the default network (net/quic/quic_chromium_client_session.cc:4081):

    if (migrate_session_on_network_change_v2_ &&
        default_network_ != handles::kInvalidNetworkHandle &&
        GetCurrentNetwork() != default_network_)
    

    For a proxied session, GetCurrentNetwork() invokes GetBoundNetwork() on the QuicProxyDatagramClientSocket, which correctly returns kInvalidNetworkHandle. Because the session was erroneously constructed with a valid default_network_ (e.g., the Wi-Fi interface handle), the condition GetCurrentNetwork() != default_network_ evaluates to true. The session then incorrectly schedules a migration back to the default network.

  3. Client IP Leak via Probing: When the migration timer fires (after kMinRetryTimeForDefaultNetworkSecs), StartProbing creates a direct, unproxied DatagramClientSocket bound to the physical default_network_. It sends a raw QUIC PATH_CHALLENGE to the session’s peer_address(). In the context of a proxied session, peer_address() is the proxy’s IP address. This sends a raw UDP packet from the client’s real IP directly to the proxy, exposing the client’s location/identity.

  4. Encapsulation Bypass via Non-Fatal Check: Because the proxy is a standard QUIC server, it responds to the PATH_CHALLENGE with a PATH_RESPONSE. This validates the direct path, triggering QuicChromiumClientSession::MigrateToSocket(). Inside MigrateToSocket (net/quic/quic_chromium_client_session.cc:4228), there is a safeguard:

    // Sessions carried via a proxy should never migrate...
    DUMP_WILL_BE_CHECK(session_key_.proxy_chain().is_direct());
    

    Crucially, DUMP_WILL_BE_CHECK is non-fatal in official release builds (DCHECK_IS_ON() is false). It merely generates a crash dump and allows execution to continue. The session proceeds to MigratePath, permanently swapping its proxy-encapsulated writer for a direct raw UDP writer. All subsequent QUIC packets bypass the CONNECT-UDP tunnel entirely.

Potential Reproduction Steps

Although not yet verified with a live exploit, an attacker or researcher could potentially observe this issue using the following steps:

  1. On an Android device or a client with kMigrateSessionsOnNetworkChangeV2 enabled, configure Chromium to route QUIC traffic through a CONNECT-UDP proxy (e.g., an IP Protection proxy).
  2. Initiate an HTTP/3 connection to any target website.
  3. Monitor the network traffic on the client’s physical network interface (e.g., using Wireshark or tcpdump).
  4. Observe the initial QUIC handshake completing over the proxy tunnel.
  5. Wait approximately 1 second (the duration of kMinRetryTimeForDefaultNetworkSecs).
  6. Observe a raw UDP PATH_CHALLENGE packet sent directly from the client’s real IP to the proxy’s IP.
  7. If the proxy relays or responds to the probe, observe that subsequent QUIC packets for the session are sent as raw UDP packets directly to the proxy, completely bypassing the HTTP/3 encapsulation of the MASQUE tunnel.

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker