CVE-2026-79208
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnet/spdy/spdy_session.cc |
modified | |
ifnet/spdy/spdy_session_unittest.cc |
modified | |
SpdySessionTestnet/spdy/spdy_session_unittest.cc |
modified | |
TEST_Fnet/spdy/spdy_session_unittest.cc |
modified |
Files Changed
net/spdy/spdy_session.ccnet/spdy/spdy_session_unittest.cc
Patch
From 9aca0bf1b0e711be9240cc79b5514dc7e2363a1e Mon Sep 17 00:00:00 2001 From: Kenichi Ishibashi <[email protected]> Date: Tue, 07 Jul 2026 22:15:34 -0700 Subject: [PATCH] Ignore HTTP/2 ALTSVC frames on sessions used as proxies SpdySession::OnAltSvc() derives the origin for a per-stream ALTSVC frame from SpdyStream::url(). On a session whose SessionUsage is kProxy, that stream is a CONNECT tunnel and url() is the tunneled destination, not an origin the session host is authoritative for per RFC 7838. Drop ALTSVC frames on such sessions instead of writing alternative service entries for the tunneled origin into HttpServerProperties. TAG=agy CONV=ea9db2ef-5f39-4244-8499-10bace633b74 Bug: 513287677 Change-Id: I67e350c2868ba931958a6c8d2319b8c1585d0aa6 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8018122 Commit-Queue: Kenichi Ishibashi <[email protected]> Reviewed-by: Stefano Duo <[email protected]> Cr-Commit-Position: refs/heads/main@{#1658495} --- diff --git a/net/spdy/spdy_session.cc b/net/spdy/spdy_session.cc index 1fa66e1..db8fcac 100644 --- a/net/spdy/spdy_session.cc +++ b/net/spdy/spdy_session.cc @@ -38,6 +38,7 @@ #include "net/base/privacy_mode.h" #include "net/base/proxy_chain.h" #include "net/base/proxy_string_util.h" +#include "net/base/session_usage.h" #include "net/base/url_util.h" #include "net/cert/asn1_util.h" #include "net/cert/cert_verify_result.h" @@ -3203,6 +3204,14 @@ spdy::SpdyStreamId stream_id, std::string_view origin, const spdy::SpdyAltSvcWireFormat::AlternativeServiceVector& altsvc_vector) { + // For sessions carrying proxy traffic, the peer is not authoritative for the + // origins associated with the carried streams. Except for stream 0, which + // must specify the origin in the ALTSVC frame itself. + // https://datatracker.ietf.org/doc/html/rfc7838#section-4. + if (spdy_session_key_.session_usage() == SessionUsage::kProxy && + stream_id != 0) { + return; + } url::SchemeHostPort scheme_host_port; if (stream_id == 0) { if (origin.empty()) diff --git a/net/spdy/spdy_session_unittest.cc b/net/spdy/spdy_session_unittest.cc index 81fe198..5a627384 100644 --- a/net/spdy/spdy_session_unittest.cc +++ b/net/spdy/spdy_session_unittest.cc @@ -20,6 +20,7 @@ #include "base/strings/string_number_conversions.h" #include "base/task/single_thread_task_runner.h" #include "base/test/metrics/histogram_tester.h" +#include "base/test/run_until.h" #include "base/test/scoped_feature_list.h" #include "base/test/task_environment.h" #include "base/time/time.h" @@ -153,6 +154,23 @@ base::OnceClosure quit_closure_; }; +void WaitForSessionCloseHelper(base::WeakPtr<SpdySession> session, + base::OnceClosure quit_closure) { + if (!session) { + std::move(quit_closure).Run(); + return; + } + base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(WaitForSessionCloseHelper, session, + std::move(quit_closure))); +} + +void WaitForSessionClose(base::WeakPtr<SpdySession> session) { + base::RunLoop run_loop; + WaitForSessionCloseHelper(session, run_loop.QuitClosure()); + run_loop.Run(); +} + } // namespace class SpdySessionTest : public PlatformTest, public WithTaskEnvironment { @@ -6122,6 +6140,10 @@ ::net::CreateSpdySession(http_session_.get(), key_, NetLogWithSource()); } + bool AllSocketDataConsumed() const { + return data_->AllReadDataConsumed() && data_->AllWriteDataConsumed(); + } + spdy::SpdyAltSvcWireFormat::AlternativeService alternative_service_; private: @@ -6455,7 +6477,9 @@ spdy_stream1->SendRequestHeaders(std::move(headers), NO_MORE_DATA_TO_SEND); - base::RunLoop().RunUntilIdle(); + WaitForSessionClose(session_); + EXPECT_TRUE(data.AllWriteDataConsumed()); + EXPECT_TRUE(data.AllReadDataConsumed()); const url::SchemeHostPort session_origin("https", test_url_.GetHost(), test_url_.EffectiveIntPort()); @@ -6471,6 +6495,106 @@ .empty()); } +// An ALTSVC frame received on a session used to carry tunnels to other +// destinations must be ignored: the session host is not authoritative for the +// origin associated with the tunnel stream. +TEST_F(AltSvcFrameTest, DoNotProcessAltSvcFrameOnProxySession) { + key_ = SpdySessionKey(HostPortPair::FromURL(test_url_), PRIVACY_MODE_DISABLED, + ProxyChain::Direct(), SessionUsage::kProxy, SocketTag(), + NetworkAnonymizationKey(), SecureDnsPolicy::kAllow, + /*disable_cert_verification_network_fetches=*/true, + handles::kInvalidNetworkHandle); + + spdy::SpdyAltSvcIR altsvc_ir(/* stream_id = */ 1); + altsvc_ir.add_altsvc(alternative_service_); + + spdy::SpdySerializedFrame altsvc_frame(spdy_util_.SerializeFrame(altsvc_ir)); + spdy::SpdySerializedFrame rst( + spdy_util_.ConstructSpdyRstStream(1, spdy::ERROR_CODE_REFUSED_STREAM)); + MockRead reads[] = { + CreateMockRead(altsvc_frame, 1), CreateMockRead(rst, 2), + MockRead(ASYNC, 0, 3) // EOF + }; + + const char request_origin[] = "https://invalid.example.org"; + spdy::SpdySerializedFrame req( + spdy_util_.ConstructSpdyGet(request_origin, 1, MEDIUM)); + MockWrite writes[] = { + CreateMockWrite(req, 0), + }; + SequencedSocketData data(reads, writes); + session_deps_.socket_factory->AddSocketDataProvider(&data); + + AddSSLSocketData(); + + CreateNetworkSession(); + CreateSpdySession(); + + base::WeakPtr<SpdyStream> spdy_stream1 = CreateStreamSynchronously( + SPDY_REQUEST_RESPONSE_STREAM, session_, GURL(request_origin), MEDIUM, + NetLogWithSource()); + test::StreamDelegateDoNothing delegate1(spdy_stream1); + spdy_stream1->SetDelegate(&delegate1); + + quiche::HttpHeaderBlock headers( + spdy_util_.ConstructGetHeaderBlock(request_origin)); + + spdy_stream1->SendRequestHeaders(std::move(headers), NO_MORE_DATA_TO_SEND); + + WaitForSessionClose(session_); + EXPECT_TRUE(data.AllWriteDataConsumed()); + EXPECT_TRUE(data.AllReadDataConsumed()); + + const url::SchemeHostPort session_origin("https", test_url_.GetHost(), + test_url_.EffectiveIntPort()); + ASSERT_TRUE(spdy_session_pool_->http_server_properties() + ->GetAlternativeServiceInfos(session_origin, + NetworkAnonymizationKey()) + .empty()); + + ASSERT_TRUE(spdy_session_pool_->http_server_properties() + ->GetAlternativeServiceInfos( + url::SchemeHostPort(GURL(request_origin)), + NetworkAnonymizationKey()) + .empty()); +} + +TEST_F(AltSvcFrameTest, ProcessAltSvcFrameOnProxySessionOnStreamZero) { + key_ = SpdySessionKey(HostPortPair::FromURL(test_url_), PRIVACY_MODE_DISABLED, + ProxyChain::Direct(), SessionUsage::kProxy, SocketTag(), + NetworkAnonymizationKey(), SecureDnsPolicy::kAllow, + /*disable_cert_verification_network_fetches=*/true, + handles::kInvalidNetworkHandle); + + const char origin[] = "https://mail.example.org"; + spdy::SpdyAltSvcIR altsvc_ir(/* stream_id = */ 0); + altsvc_ir.add_altsvc(alternative_service_); + altsvc_ir.set_origin(origin); + AddSocketData(altsvc_ir); + AddSSLSocketData(); + + CreateNetworkSession(); + CreateSpdySession(); + + WaitForSessionClose(session_); + EXPECT_TRUE(AllSocketDataConsumed()); + + const url::SchemeHostPort session_origin("https", test_url_.GetHost(), + test_url_.EffectiveIntPort()); + AlternativeServiceInfoVector altsvc_info_vector = + spdy_session_pool_->http_server_properties()->GetAlternativeServiceInfos(
Regression Test / PoC
diff --git a/net/spdy/spdy_session_unittest.cc b/net/spdy/spdy_session_unittest.cc
index 81fe198..5a627384 100644
--- a/net/spdy/spdy_session_unittest.cc
+++ b/net/spdy/spdy_session_unittest.cc
@@ -20,6 +20,7 @@
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
+#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
@@ -153,6 +154,23 @@
base::OnceClosure quit_closure_;
};
+void WaitForSessionCloseHelper(base::WeakPtr<SpdySession> session,
+ base::OnceClosure quit_closure) {
+ if (!session) {
+ std::move(quit_closure).Run();
+ return;
+ }
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, base::BindOnce(WaitForSessionCloseHelper, session,
+ std::move(quit_closure)));
+}
+
+void WaitForSessionClose(base::WeakPtr<SpdySession> session) {
+ base::RunLoop run_loop;
+ WaitForSessionCloseHelper(session, run_loop.QuitClosure());
+ run_loop.Run();
+}
+
} // namespace
class SpdySessionTest : public PlatformTest, public WithTaskEnvironment {
@@ -6122,6 +6140,10 @@
::net::CreateSpdySession(http_session_.get(), key_, NetLogWithSource());
}
+ bool AllSocketDataConsumed() const {
+ return data_->AllReadDataConsumed() && data_->AllWriteDataConsumed();
+ }
+
spdy::SpdyAltSvcWireFormat::AlternativeService alternative_service_;
private:
@@ -6455,7 +6477,9 @@
spdy_stream1->SendRequestHeaders(std::move(headers), NO_MORE_DATA_TO_SEND);
- base::RunLoop().RunUntilIdle();
+ WaitForSessionClose(session_);
+ EXPECT_TRUE(data.AllWriteDataConsumed());
+ EXPECT_TRUE(data.AllReadDataConsumed());
const url::SchemeHostPort session_origin("https", test_url_.GetHost(),
test_url_.EffectiveIntPort());
@@ -6471,6 +6495,106 @@
.empty());
}
+// An ALTSVC frame received on a session used to carry tunnels to other
+// destinations must be ignored: the session host is not authoritative for the
+// origin associated with the tunnel stream.
+TEST_F(AltSvcFrameTest, DoNotProcessAltSvcFrameOnProxySession) {
+ key_ = SpdySessionKey(HostPortPair::FromURL(test_url_), PRIVACY_MODE_DISABLED,
+ ProxyChain::Direct(), SessionUsage::kProxy, SocketTag(),
+ NetworkAnonymizationKey(), SecureDnsPolicy::kAllow,
+ /*disable_cert_verification_network_fetches=*/true,
+ handles::kInvalidNetworkHandle);
+
+ spdy::SpdyAltSvcIR altsvc_ir(/* stream_id = */ 1);
+ altsvc_ir.add_altsvc(alternative_service_);
+
+ spdy::SpdySerializedFrame altsvc_frame(spdy_util_.SerializeFrame(altsvc_ir));
+ spdy::SpdySerializedFrame rst(
+ spdy_util_.ConstructSpdyRstStream(1, spdy::ERROR_CODE_REFUSED_STREAM));
+ MockRead reads[] = {
+ CreateMockRead(altsvc_frame, 1), CreateMockRead(rst, 2),
+ MockRead(ASYNC, 0, 3) // EOF
+ };
+
+ const char request_origin[] = "https://invalid.example.org";
+ spdy::SpdySerializedFrame req(
+ spdy_util_.ConstructSpdyGet(request_origin, 1, MEDIUM));
+ MockWrite writes[] = {
+ CreateMockWrite(req, 0),
+ };
+ SequencedSocketData data(reads, writes);
+ session_deps_.socket_factory->AddSocketDataProvider(&data);
+
+ AddSSLSocketData();
+
+ CreateNetworkSession();
+ CreateSpdySession();
+
+ base::WeakPtr<SpdyStream> spdy_stream1 = CreateStreamSynchronously(
+ SPDY_REQUEST_RESPONSE_STREAM, session_, GURL(request_origin), MEDIUM,
+ NetLogWithSource());
+ test::StreamDelegateDoNothing delegate1(spdy_stream1);
+ spdy_stream1->SetDelegate(&delegate1);
+
+ quiche::HttpHeaderBlock headers(
+ spdy_util_.ConstructGetHeaderBlock(request_origin));
+
+ spdy_stream1->SendRequestHeaders(std::move(headers), NO_MORE_DATA_TO_SEND);
+
+ WaitForSessionClose(session_);
+ EXPECT_TRUE(data.AllWriteDataConsumed());
+ EXPECT_TRUE(data.AllReadDataConsumed());
+
+ const url::SchemeHostPort session_origin("https", test_url_.GetHost(),
+ test_url_.EffectiveIntPort());
+ ASSERT_TRUE(spdy_session_pool_->http_server_properties()
+ ->GetAlternativeServiceInfos(session_origin,
+ NetworkAnonymizationKey())
+ .empty());
+
+ ASSERT_TRUE(spdy_session_pool_->http_server_properties()
+ ->GetAlternativeServiceInfos(
+ url::SchemeHostPort(GURL(request_origin)),
+ NetworkAnonymizationKey())
+ .empty());
+}
+
+TEST_F(AltSvcFrameTest, ProcessAltSvcFrameOnProxySessionOnStreamZero) {
+ key_ = SpdySessionKey(HostPortPair::FromURL(test_url_), PRIVACY_MODE_DISABLED,
+ ProxyChain::Direct(), SessionUsage::kProxy, SocketTag(),
+ NetworkAnonymizationKey(), SecureDnsPolicy::kAllow,
+ /*disable_cert_verification_network_fetches=*/true,
+ handles::kInvalidNetworkHandle);
+
+ const char origin[] = "https://mail.example.org";
+ spdy::SpdyAltSvcIR altsvc_ir(/* stream_id = */ 0);
+ altsvc_ir.add_altsvc(alternative_service_);
+ altsvc_ir.set_origin(origin);
+ AddSocketData(altsvc_ir);
+ AddSSLSocketData();
+
+ CreateNetworkSession();
+ CreateSpdySession();
+
+ WaitForSessionClose(session_);
+ EXPECT_TRUE(AllSocketDataConsumed());
+
+ const url::SchemeHostPort session_origin("https", test_url_.GetHost(),
+ test_url_.EffectiveIntPort());
+ AlternativeServiceInfoVector altsvc_info_vector =
+ spdy_session_pool_->http_server_properties()->GetAlternativeServiceInfos(
+ session_origin, NetworkAnonymizationKey());
+ ASSERT_TRUE(altsvc_info_vector.empty());
+
+ altsvc_info_vector =
+ spdy_session_pool_->http_server_properties()->GetAlternativeServiceInfos(
+ url::SchemeHostPort(GURL(origin)), NetworkAnonymizationKey());
+ ASSERT_EQ(1u, altsvc_info_vector.size());
+ AlternativeService alternative_service(NextProto::kProtoQUIC,
+ "alternative.example.org", 443u);
+ EXPECT_EQ(alternative_service, altsvc_info_vector[0].alternative_service());
+}
+
TEST_F(AltSvcFrameTest, DoNotProcessAltSvcFrameOnNonExistentStream) {
spdy::SpdyAltSvcIR altsvc_ir(/* stream_id = */ 1);
altsvc_ir.add_altsvc(alternative_service_);
Original Bug Report
Alt-Svc cache poisoning by malicious HTTP/2 proxy for tunneled origins
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A malicious HTTP/2 proxy can inject unauthorized Alt-Svc entries for any origin tunneled through a CONNECT stream. This occurs because SpdySession::OnAltSvc fails to perform a certificate authority check when processing ALTSVC frames on non-zero stream IDs. Injected entries persist in the Alt-Svc cache even after the proxy is removed, enabling cross-network tracking and browsing history inference.
Affected files:
net/spdy/spdy_session.ccnet/http/http_proxy_connect_job.ccnet/spdy/spdy_stream.cc
Estimated timestamp from git blame: 2016-06-03
Summary
A potential vulnerability exists in Chrome’s HTTP/2 implementation where a malicious or compromised HTTPS proxy can inject Alt-Svc entries for arbitrary origins tunneled through it. This is caused by a missing authority check in SpdySession::OnAltSvc when handling ALTSVC frames received on active CONNECT streams.
Root Cause Analysis
In net/spdy/spdy_session.cc, the SpdySession::OnAltSvc method handles ALTSVC frames. The logic handles session-level (stream 0) and stream-level (non-zero stream ID) frames differently:
- For
stream_id == 0: The code correctly verifies that the session’s certificate is authorized for the origin specified in theALTSVCframe using aCanPoolcheck (line 3218). - For
stream_id != 0: The code retrieves the URL associated with the active stream. ForCONNECTstreams used by an HTTP/2 proxy, this URL is synthesized as the destination origin (e.g.,https://victim.com:443) inHttpProxyConnectJob::DoSpdyProxyCreateStream(line 704 ofnet/http/http_proxy_connect_job.cc). However, the implementation inSpdySession::OnAltSvconly checks if the scheme is HTTPS (line 3230) and fails to perform any certificate authority check (likeCanPool) to ensure the proxy is authorized to provide alternative services for that destination origin.
Because HttpServerProperties (the Alt-Svc cache) is keyed by (origin, NetworkAnonymizationKey) and does not include the proxy chain, entries injected while using a proxy persist and are applied when the user later connects to the same origin directly without the proxy.
Suggested Steps to Reproduce (Theoretical)
- Configure Chrome to use an HTTPS proxy that negotiates
h2and is trusted by the client. - Navigate to a destination site (e.g.,
https://example.com) through the proxy. - The proxy, upon receiving the
CONNECTrequest, sends anALTSVCframe on the same stream ID (e.g., Stream 1) with a value likeh3="attacker.test:443"; ma=2592000. SpdySession::OnAltSvcprocesses the frame and updatesHttpServerPropertieswith the attacker-supplied alternative service forexample.combecause it lacks an authority check for Stream 1.- Observe the entry in
chrome://net-internals/#alt-svc. - Disable the proxy and navigate to
https://example.comdirectly. Chrome will attempt a QUIC connection toattacker.testwith SNIexample.com, leaking the user’s IP and browsing activity.
Impact
This enables persistent tracking of a user’s browsing history even after they stop using the malicious proxy. The attacker-controlled host receives connection attempts (including SNI) from the user’s direct IP whenever they visit a poisoned origin. An attacker could also use this to persistently disable QUIC for specific origins by sending a ‘clear’ Alt-Svc value.
Suggested Fix
Modify SpdySession::OnAltSvc to perform an authority check for non-zero stream IDs. Specifically, if the session is used as a proxy (indicated by the ProxyChain or SessionUsage in the SpdySessionKey), it should not accept ALTSVC frames on CONNECT streams, or it should strictly verify that the session has certificate authority for the stream’s origin before updating HttpServerProperties.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.