CVE-2026-14054
Overview
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::JobControllerand 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.
Alt-Svc port unconditionally; the fix restores parity by gating alternative-job startup on IsPortAllowedForScheme().Attack Path
- Enable the vulnerable path
The victim runs a build with HappyEyeballsV3 enabled, routing connections through
HttpStreamPool::JobController. - Advertise a malicious alternative service
A malicious or compromised origin returns an
Alt-Svcheader pointing HTTP/2 traffic at a restricted port such asalt.example.org:10080or port25. - Trigger alternative-job startup
On the next request,
MaybeStartAlternativeJob()accepts the advertisedalternative_->stream_keyand reachespool_->GetOrCreateGroup()with no port validation. - 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
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
| Function | Change | Notes |
|---|---|---|
TEST_Fnet/http/http_stream_pool_attempt_manager_unittest.cc |
modified |
Files Changed
net/http/http_stream_pool_attempt_manager_unittest.ccnet/http/http_stream_pool_job_controller.ccnet/log/net_log_event_type_list.h
Audit Directions
- Divergent connection pathsAudit 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 destinationsReview every place an
Alt-Svc, redirect, or other server-supplied host/port is consumed to confirmIsPortAllowedForScheme()(or equivalent) runs before a socket group is created. - Missing pre-connect validationSearch for
GetOrCreateGroup()and similar connection-initiation calls reached without prior port/scheme policy enforcement.
Patch
From f40c463966b285f51660b4c7a2758f27590f49f0 Mon Sep 17 00:00:00 2001 From: Kenichi Ishibashi <[email protected]> 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 <[email protected]> Reviewed-by: mmenke <[email protected]> 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)
Regression Test / PoC
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);