Low watch CVSS 4.3 chrome Policy Bypass 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Network
DescriptionInsufficient policy enforcement in Network
ComponentNetwork
Sub-componentNet
Bug ClassPolicy Bypass
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker501851312
Fix commitf40c463966b2 (chromium/src) +67/-0
CWECWE-602
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-12 (per vendor advisory)
Fix landed2026-05-13 (public git)
Disclosed2026-06-30
Patch gap48 days — fix public in source before the advisory

Background

`HttpStreamPool::JobController`
The network-stack controller that decides whether a request connects to its origin or to an advertised alternative service.
Alt-Svc (alternative service)
An HTTP mechanism by which an origin advertises a different host/port/protocol (e.g. HTTP/2 on another port) that clients may use to reach the same content.
HappyEyeballsV3
A connection-racing feature that, when enabled, routes stream establishment through HttpStreamPool::JobController and its alternative-job path.
`IsPortAllowedForScheme()`
A net utility that returns whether a given port is permitted for a scheme, blocking known cross-protocol-attack ports (e.g. 25, 10080).

Root Cause Analysis

When HappyEyeballsV3 is enabled, HttpStreamPool::JobController::MaybeStartAlternativeJob() created an alternative-service job directly from the Alt-Svc-advertised destination, calling pool_->GetOrCreateGroup() on alternative_->stream_key without ever validating the advertised port. The invariant violated is that the network stack must never open connections to restricted ports, since those ports host non-HTTP services vulnerable to cross-protocol attacks; the origin-connection path enforced this, but the newer alternative-job path did not. Because the port is attacker-controlled through the Alt-Svc header, a malicious origin could steer the browser to a restricted port such as 25 or 10080.

The fix inserts an IsPortAllowedForScheme() check comparing alternative_->stream_key.alt_service()->port against alternative_->stream_key.destination().scheme() before the group is created. If the port is disallowed, the method emits a HTTP_STREAM_POOL_JOB_CONTROLLER_SKIPPED_ALTSVC_RESTRICTED_PORT NetLog event and returns false, causing the request to fall back safely to the origin connection.

Key insight
The single mistake was that the HappyEyeballsV3 alternative-service code path omitted the restricted-port policy check that the origin path already applied, trusting the attacker-supplied Alt-Svc port unconditionally; the fix restores parity by gating alternative-job startup on IsPortAllowedForScheme().

Attack Path

  1. Enable the vulnerable path The victim runs a build with HappyEyeballsV3 enabled, routing connections through HttpStreamPool::JobController.
  2. Advertise a malicious alternative service A malicious or compromised origin returns an Alt-Svc header pointing HTTP/2 traffic at a restricted port such as alt.example.org:10080 or port 25.
  3. Trigger alternative-job startup On the next request, MaybeStartAlternativeJob() accepts the advertised alternative_->stream_key and reaches pool_->GetOrCreateGroup() with no port validation.
  4. Redirect traffic to a restricted service The browser opens a connection to the restricted port, letting the attacker smuggle HTTP-looking bytes into a non-HTTP service and bypass cross-protocol attack mitigations.

Impact Assessment

An attacker controlling an origin’s Alt-Svc advertisement gains the ability to make the victim’s browser initiate connections to restricted ports (e.g. 25, 10080) from within the network process, enabling cross-protocol attacks against services normally shielded by Chrome’s port-blocking policy. The precondition is that HappyEyeballsV3 is enabled and the victim visits or is redirected to the attacker-controlled origin; no memory corruption is required, as this is a logic/policy-enforcement flaw.

Changed Functions

FunctionChangeNotes
TEST_F
net/http/http_stream_pool_attempt_manager_unittest.cc
modified

Files Changed

  • net/http/http_stream_pool_attempt_manager_unittest.cc
  • net/http/http_stream_pool_job_controller.cc
  • net/log/net_log_event_type_list.h

Audit Directions

  • Divergent connection paths
    Audit newer connection-racing code (HappyEyeballsV3, HttpStreamPool) against the legacy origin path to find security checks like port restriction, HSTS, or scheme validation that were not carried over.
  • Attacker-controlled destinations
    Review every place an Alt-Svc, redirect, or other server-supplied host/port is consumed to confirm IsPortAllowedForScheme() (or equivalent) runs before a socket group is created.
  • Missing pre-connect validation
    Search for GetOrCreateGroup() and similar connection-initiation calls reached without prior port/scheme policy enforcement.
From f40c463966b285f51660b4c7a2758f27590f49f0 Mon Sep 17 00:00:00 2001
From: Kenichi Ishibashi <bashi@chromium.org>
Date: Wed, 13 May 2026 16:21:40 -0700
Subject: [PATCH] net: Validate restricted ports for Alt-Svc in HttpStreamPool

When HappyEyeballsV3 is enabled, HttpStreamPool::JobController starts
alternative service jobs without validating whether the destination port
advertised by Alt-Svc is restricted. This allows a malicious origin to
bypass cross-protocol attack mitigations by redirecting traffic to
restricted ports (e.g., port 25, 10080).

This CL adds IsPortAllowedForScheme() check to
HttpStreamPool::JobController::MaybeStartAlternativeJob(). If the
alternative service's port is restricted, the connection attempt to the
alternative service is skipped, and the request safely falls back to the
origin connection.

Bug: 501851312
Change-Id: Ie7913f5559372801379d5010529a8974af10c3be
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7839160
Commit-Queue: Kenichi Ishibashi <bashi@chromium.org>
Reviewed-by: mmenke <mmenke@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1630303}
---

diff --git a/net/http/http_stream_pool_attempt_manager_unittest.cc b/net/http/http_stream_pool_attempt_manager_unittest.cc
index fb464d2a9..6029fba 100644
--- a/net/http/http_stream_pool_attempt_manager_unittest.cc
+++ b/net/http/http_stream_pool_attempt_manager_unittest.cc
@@ -562,6 +562,8 @@
     return quic::ParsedQuicVersion::RFCv1();
   }
 
+  RecordingNetLogObserver& net_log_observer() { return net_log_observer_; }
+
   base::WeakPtr<SpdySession> CreateFakeSpdySession(
       const HttpStreamKey& stream_key,
       IPEndPoint peer_addr = IPEndPoint(IPAddress(192, 0, 2, 1), 443)) {
@@ -6846,6 +6848,48 @@
       alternative_service, NetworkAnonymizationKey()));
 }
 
+// Tests that if an alternative service destination uses a restricted port,
+// connection attempt to the alternative service is skipped and the request
+// falls back to the origin connection.
+// Regression test for crbug.com/501851312
+TEST_F(HttpStreamPoolAttemptManagerTest, AltSvcH2UnsafePort) {
+  const url::SchemeHostPort kOrigin(url::kHttpsScheme, "origin.example.org",
+                                    443);
+  const HostPortPair kAlternative("alt.example.org", 10080);
+
+  const AlternativeService alternative_service(NextProto::kProtoHTTP2,
+                                               kAlternative);
+  const base::Time expiration = base::Time::Now() + base::Days(1);
+
+  StreamRequester requester;
+  requester.set_destination(kOrigin).set_alternative_service_info(
+      AlternativeServiceInfo::CreateHttp2AlternativeServiceInfo(
+          alternative_service, expiration));
+
+  resolver()
+      ->AddFakeRequest()
+      ->add_endpoint(ServiceEndpointBuilder().add_v4("192.0.2.1").endpoint())
+      .CompleteStartSynchronously(OK);
+
+  // For the origin. The connection is refused.
+  StaticSocketDataProvider origin_data;
+  origin_data.set_connect_data(MockConnect(ASYNC, ERR_CONNECTION_REFUSED));
+  socket_factory()->AddSocketDataProvider(&origin_data);
+
+  requester.RequestStream(pool());
+  requester.WaitForResult();
+
+  // The alternative job is not started because of the unsafe port,
+  // and the origin job fails with ERR_CONNECTION_REFUSED.
+  EXPECT_THAT(requester.result(), Optional(IsError(ERR_CONNECTION_REFUSED)));
+
+  auto entries = net_log_observer().GetEntriesWithType(
+      NetLogEventType::
+          HTTP_STREAM_POOL_JOB_CONTROLLER_SKIPPED_ALTSVC_RESTRICTED_PORT);
+  ASSERT_EQ(entries.size(), 1u);
+  EXPECT_THAT(entries[0].params.FindInt("port"), Optional(10080));
+}
+
 TEST_F(HttpStreamPoolAttemptManagerTest, AltSvcFailOriginOk) {
   const url::SchemeHostPort kOrigin(url::kHttpsScheme, "origin.example.org",
                                     443);
diff --git a/net/http/http_stream_pool_job_controller.cc b/net/http/http_stream_pool_job_controller.cc
index e086b37..d827275 100644
--- a/net/http/http_stream_pool_job_controller.cc
+++ b/net/http/http_stream_pool_job_controller.cc
@@ -613,6 +613,21 @@
     return false;
   }
 
+  CHECK(alternative_->stream_key.alt_service().has_value());
+  if (!IsPortAllowedForScheme(
+          alternative_->stream_key.alt_service()->port,
+          alternative_->stream_key.destination().scheme())) {
+    net_log_.AddEvent(
+        NetLogEventType::
+            HTTP_STREAM_POOL_JOB_CONTROLLER_SKIPPED_ALTSVC_RESTRICTED_PORT,
+        [&] {
+          base::DictValue dict;
+          dict.Set("port", alternative_->stream_key.alt_service()->port);
+          return dict;
+        });
+    return false;
+  }
+
   Group& alternative_group = pool_->GetOrCreateGroup(alternative_->stream_key);
 
   // We never put streams that are negotiated to use HTTP/2 as idle streams.
diff --git a/net/log/net_log_event_type_list.h b/net/log/net_log_event_type_list.h
index 5d59f4a..f0b042b 100644
--- a/net/log/net_log_event_type_list.h
+++ b/net/log/net_log_event_type_list.h
@@ -1613,6 +1613,14 @@
 //   }
 EVENT_TYPE(HTTP_STREAM_POOL_JOB_CONTROLLER_ALIVE)
 
+// Emitted when an HttpStreamPool::JobController skips an alternative service
+// because its port is not allowed for the scheme.
+// The event parameters are:
+//   {
+//      "port": <The port of the alternative service>,
+//   }
+EVENT_TYPE(HTTP_STREAM_POOL_JOB_CONTROLLER_SKIPPED_ALTSVC_RESTRICTED_PORT)
+
 // Emitted when an HttpStreamPool::JobController found an existing SPDY session.
 EVENT_TYPE(HTTP_STREAM_POOL_JOB_CONTROLLER_FOUND_EXISTING_SPDY_SESSION)
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/net/http/http_stream_pool_attempt_manager_unittest.cc b/net/http/http_stream_pool_attempt_manager_unittest.cc
index fb464d2a9..6029fba 100644
--- a/net/http/http_stream_pool_attempt_manager_unittest.cc
+++ b/net/http/http_stream_pool_attempt_manager_unittest.cc
@@ -562,6 +562,8 @@
     return quic::ParsedQuicVersion::RFCv1();
   }
 
+  RecordingNetLogObserver& net_log_observer() { return net_log_observer_; }
+
   base::WeakPtr<SpdySession> CreateFakeSpdySession(
       const HttpStreamKey& stream_key,
       IPEndPoint peer_addr = IPEndPoint(IPAddress(192, 0, 2, 1), 443)) {
@@ -6846,6 +6848,48 @@
       alternative_service, NetworkAnonymizationKey()));
 }
 
+// Tests that if an alternative service destination uses a restricted port,
+// connection attempt to the alternative service is skipped and the request
+// falls back to the origin connection.
+// Regression test for crbug.com/501851312
+TEST_F(HttpStreamPoolAttemptManagerTest, AltSvcH2UnsafePort) {
+  const url::SchemeHostPort kOrigin(url::kHttpsScheme, "origin.example.org",
+                                    443);
+  const HostPortPair kAlternative("alt.example.org", 10080);
+
+  const AlternativeService alternative_service(NextProto::kProtoHTTP2,
+                                               kAlternative);
+  const base::Time expiration = base::Time::Now() + base::Days(1);
+
+  StreamRequester requester;
+  requester.set_destination(kOrigin).set_alternative_service_info(
+      AlternativeServiceInfo::CreateHttp2AlternativeServiceInfo(
+          alternative_service, expiration));
+
+  resolver()
+      ->AddFakeRequest()
+      ->add_endpoint(ServiceEndpointBuilder().add_v4("192.0.2.1").endpoint())
+      .CompleteStartSynchronously(OK);
+
+  // For the origin. The connection is refused.
+  StaticSocketDataProvider origin_data;
+  origin_data.set_connect_data(MockConnect(ASYNC, ERR_CONNECTION_REFUSED));
+  socket_factory()->AddSocketDataProvider(&origin_data);
+
+  requester.RequestStream(pool());
+  requester.WaitForResult();
+
+  // The alternative job is not started because of the unsafe port,
+  // and the origin job fails with ERR_CONNECTION_REFUSED.
+  EXPECT_THAT(requester.result(), Optional(IsError(ERR_CONNECTION_REFUSED)));
+
+  auto entries = net_log_observer().GetEntriesWithType(
+      NetLogEventType::
+          HTTP_STREAM_POOL_JOB_CONTROLLER_SKIPPED_ALTSVC_RESTRICTED_PORT);
+  ASSERT_EQ(entries.size(), 1u);
+  EXPECT_THAT(entries[0].params.FindInt("port"), Optional(10080));
+}
+
 TEST_F(HttpStreamPoolAttemptManagerTest, AltSvcFailOriginOk) {
   const url::SchemeHostPort kOrigin(url::kHttpsScheme, "origin.example.org",
                                     443);
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.