CVE-2026-79220
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
WebSocketFactoryHandshakeTestservices/network/websocket_factory_unittest.cc |
modified | |
WebSocketFactoryHandshakeTestservices/network/websocket_factory_unittest.cc |
modified |
Files Changed
services/network/websocket.ccservices/network/websocket_factory_unittest.cc
Patch
From cc8758dc9c6b524b778d6e58a469903b3e60ed96 Mon Sep 17 00:00:00 2001
From: Martin Verde <thesalsa@google.com>
Date: Tue, 30 Jun 2026 21:24:29 -0700
Subject: [PATCH] Filter auth headers from WebSocket handshake reported to client
OnStartOpeningHandshake reports the over-the-wire handshake request back
to the WebSocketHandshakeClient. For clients without raw-headers access
it already drops Cookie; do the same for Authorization and
Proxy-Authorization, which the network stack may attach from
HttpAuthCache.
Add WebSocketFactoryHandshakeTest exercising the reported handshake
request against an EmbeddedTestServer with cached Basic credentials,
with and without raw-headers access.
Bug: 522823211
Change-Id: I5d84a5c719f4335a7bb65a4a7da8f0cd4e887e78
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8018463
Reviewed-by: Adam Rice <ricea@chromium.org>
Commit-Queue: Martin Pan-Verde <thesalsa@google.com>
Cr-Commit-Position: refs/heads/main@{#1655256}
---
diff --git a/services/network/websocket.cc b/services/network/websocket.cc
index 830cbe53..d16f3cc 100644
--- a/services/network/websocket.cc
+++ b/services/network/websocket.cc
@@ -488,8 +488,12 @@
net::HttpRequestHeaders::Iterator it(request->headers);
while (it.GetNext()) {
if (!impl_->has_raw_headers_access_ &&
- base::EqualsCaseInsensitiveASCII(it.name(),
- net::HttpRequestHeaders::kCookie)) {
+ (base::EqualsCaseInsensitiveASCII(it.name(),
+ net::HttpRequestHeaders::kCookie) ||
+ base::EqualsCaseInsensitiveASCII(
+ it.name(), net::HttpRequestHeaders::kAuthorization) ||
+ base::EqualsCaseInsensitiveASCII(
+ it.name(), net::HttpRequestHeaders::kProxyAuthorization))) {
continue;
}
mojom::HttpHeaderPtr header(mojom::HttpHeader::New());
diff --git a/services/network/websocket_factory_unittest.cc b/services/network/websocket_factory_unittest.cc
index f5844b3..3a40645 100644
--- a/services/network/websocket_factory_unittest.cc
+++ b/services/network/websocket_factory_unittest.cc
@@ -4,24 +4,38 @@
#include "services/network/websocket_factory.h"
+#include <algorithm>
#include <memory>
#include <string>
#include <vector>
#include "base/functional/callback.h"
+#include "base/strings/string_util.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/system/functions.h"
+#include "net/base/auth.h"
#include "net/base/isolation_info.h"
+#include "net/http/http_auth.h"
+#include "net/http/http_auth_cache.h"
+#include "net/http/http_network_session.h"
+#include "net/http/http_request_headers.h"
+#include "net/http/http_transaction_factory.h"
#include "net/log/net_log.h"
#include "net/log/net_log_entry.h"
#include "net/log/net_log_event_type.h"
+#include "net/test/embedded_test_server/create_websocket_handler.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/install_default_websocket_handlers.h"
+#include "net/test/embedded_test_server/websocket_echo_handler.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "net/url_request/url_request_context.h"
#include "services/network/network_context.h"
#include "services/network/network_service.h"
#include "services/network/public/cpp/constants.h"
+#include "services/network/public/cpp/originating_process_id.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/websocket.mojom.h"
#include "services/network/test/fake_test_cert_verifier_params_factory.h"
@@ -61,7 +75,9 @@
: receiver_(this, std::move(receiver)) {}
~StubWebSocketHandshakeClient() override = default;
- void OnOpeningHandshakeStarted(mojom::WebSocketHandshakeRequestPtr) override {
+ void OnOpeningHandshakeStarted(
+ mojom::WebSocketHandshakeRequestPtr request) override {
+ handshake_request_future_.SetValue(std::move(request));
}
void OnConnectionEstablished(mojo::PendingRemote<mojom::WebSocket>,
@@ -72,8 +88,14 @@
void OnFailure(const std::string&, int32_t, int32_t) override {}
+ mojom::WebSocketHandshakeRequestPtr WaitForOpeningHandshake() {
+ return handshake_request_future_.Take();
+ }
+
private:
mojo::Receiver<mojom::WebSocketHandshakeClient> receiver_;
+ base::test::TestFuture<mojom::WebSocketHandshakeRequestPtr>
+ handshake_request_future_;
};
mojom::NetworkContextParamsPtr CreateNetworkContextParams() {
@@ -97,8 +119,11 @@
factory_ = std::make_unique<WebSocketFactory>(network_context_.get());
}
- void CreateWebSocket(const GURL& url,
- const std::vector<std::string>& requested_protocols) {
+ StubWebSocketHandshakeClient* CreateWebSocket(
+ const GURL& url,
+ const std::vector<std::string>& requested_protocols,
+ const OriginatingProcessId& process_id =
+ OriginatingProcessId::browser()) {
mojo::PendingRemote<mojom::WebSocketHandshakeClient> handshake_client;
// Keep all handshake clients alive to prevent WebSocket cleanup
stub_handshake_clients_.push_back(
@@ -109,13 +134,13 @@
// asynchronously.
factory_->CreateWebSocket(
url, requested_protocols, net::StorageAccessApiStatus::kNone,
- net::IsolationInfo(), {}, network::OriginatingProcessId::browser(),
- url::Origin::Create(url),
+ net::IsolationInfo(), {}, process_id, url::Origin::Create(url),
/*client_security_state=*/nullptr, /*options=*/0,
TRAFFIC_ANNOTATION_FOR_TESTS, std::move(handshake_client),
mojo::NullRemote(), mojo::NullRemote(), mojo::NullRemote(),
/*throttling_profile_id=*/std::nullopt,
network::GetTestNetworkRestrictionsId());
+ return stub_handshake_clients_.back().get();
}
protected:
@@ -196,6 +221,75 @@
}
}
+bool ContainsHeader(const mojom::WebSocketHandshakeRequestPtr& request,
+ std::string_view name) {
+ return std::ranges::any_of(request->headers, [&](const auto& header) {
+ return base::EqualsCaseInsensitiveASCII(header->name, name);
+ });
+}
+
+class WebSocketFactoryHandshakeTest : public WebSocketFactoryTest {
+ public:
+ WebSocketFactoryHandshakeTest() {
+ net::test_server::RegisterWebSocketHandler<
+ net::test_server::WebSocketEchoHandler>(&test_server_, "/echo");
+ EXPECT_TRUE(test_server_.Start());
+ }
+
+ void AddBasicAuthCacheEntry() {
+ net::HttpAuthCache* cache = network_context_->url_request_context()
+ ->http_transaction_factory()
+ ->GetSession()
+ ->http_auth_cache();
+ cache->Add(url::SchemeHostPort(test_server_.base_url()),
+ net::HttpAuth::AUTH_SERVER, "TestRealm",
+ net::HttpAuth::AUTH_SCHEME_BASIC, net::NetworkAnonymizationKey(),
+ "Basic realm=TestRealm",
+ net::AuthCredentials(u"user", u"password"), "/");
+ }
+
+ protected:
+ net::EmbeddedTestServer test_server_;
+};
+
+// Verifies that the handshake request reported to a client with raw headers
+// access includes credential headers attached by the network stack.
+TEST_F(WebSocketFactoryHandshakeTest,
+ OpeningHandshakeReportsAuthHeadersWithRawHeadersAccess) {
+ AddBasicAuthCacheEntry();
+
+ StubWebSocketHandshakeClient* client = CreateWebSocket(
+ net::test_server::GetWebSocketURL(test_server_, "/echo"), {});
+
+ mojom::WebSocketHandshakeRequestPtr request =
+ client->WaitForOpeningHandshake();
+ ASSERT_TRUE(request);
+ EXPECT_TRUE(ContainsHeader(request, net::HttpRequestHeaders::kAuthorization));
+}
+
+// Verifies that the handshake request reported to a client without raw
+// headers access omits credential headers attached by the network stack.
+TEST_F(WebSocketFactoryHandshakeTest,
+ OpeningHandshakeOmitsAuthHeadersWithoutRawHeadersAccess) {
+ AddBasicAuthCacheEntry();
+
+ StubWebSocketHandshakeClient* client =
+ CreateWebSocket(net::test_server::GetWebSocketURL(test_server_, "/echo"),
+ {}, OriginatingProcessId::renderer(RendererProcessId(1)));
Regression Test / PoC
diff --git a/services/network/websocket_factory_unittest.cc b/services/network/websocket_factory_unittest.cc
index f5844b3..3a40645 100644
--- a/services/network/websocket_factory_unittest.cc
+++ b/services/network/websocket_factory_unittest.cc
@@ -4,24 +4,38 @@
#include "services/network/websocket_factory.h"
+#include <algorithm>
#include <memory>
#include <string>
#include <vector>
#include "base/functional/callback.h"
+#include "base/strings/string_util.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/system/functions.h"
+#include "net/base/auth.h"
#include "net/base/isolation_info.h"
+#include "net/http/http_auth.h"
+#include "net/http/http_auth_cache.h"
+#include "net/http/http_network_session.h"
+#include "net/http/http_request_headers.h"
+#include "net/http/http_transaction_factory.h"
#include "net/log/net_log.h"
#include "net/log/net_log_entry.h"
#include "net/log/net_log_event_type.h"
+#include "net/test/embedded_test_server/create_websocket_handler.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/install_default_websocket_handlers.h"
+#include "net/test/embedded_test_server/websocket_echo_handler.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "net/url_request/url_request_context.h"
#include "services/network/network_context.h"
#include "services/network/network_service.h"
#include "services/network/public/cpp/constants.h"
+#include "services/network/public/cpp/originating_process_id.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/websocket.mojom.h"
#include "services/network/test/fake_test_cert_verifier_params_factory.h"
@@ -61,7 +75,9 @@
: receiver_(this, std::move(receiver)) {}
~StubWebSocketHandshakeClient() override = default;
- void OnOpeningHandshakeStarted(mojom::WebSocketHandshakeRequestPtr) override {
+ void OnOpeningHandshakeStarted(
+ mojom::WebSocketHandshakeRequestPtr request) override {
+ handshake_request_future_.SetValue(std::move(request));
}
void OnConnectionEstablished(mojo::PendingRemote<mojom::WebSocket>,
@@ -72,8 +88,14 @@
void OnFailure(const std::string&, int32_t, int32_t) override {}
+ mojom::WebSocketHandshakeRequestPtr WaitForOpeningHandshake() {
+ return handshake_request_future_.Take();
+ }
+
private:
mojo::Receiver<mojom::WebSocketHandshakeClient> receiver_;
+ base::test::TestFuture<mojom::WebSocketHandshakeRequestPtr>
+ handshake_request_future_;
};
mojom::NetworkContextParamsPtr CreateNetworkContextParams() {
@@ -97,8 +119,11 @@
factory_ = std::make_unique<WebSocketFactory>(network_context_.get());
}
- void CreateWebSocket(const GURL& url,
- const std::vector<std::string>& requested_protocols) {
+ StubWebSocketHandshakeClient* CreateWebSocket(
+ const GURL& url,
+ const std::vector<std::string>& requested_protocols,
+ const OriginatingProcessId& process_id =
+ OriginatingProcessId::browser()) {
mojo::PendingRemote<mojom::WebSocketHandshakeClient> handshake_client;
// Keep all handshake clients alive to prevent WebSocket cleanup
stub_handshake_clients_.push_back(
@@ -109,13 +134,13 @@
// asynchronously.
factory_->CreateWebSocket(
url, requested_protocols, net::StorageAccessApiStatus::kNone,
- net::IsolationInfo(), {}, network::OriginatingProcessId::browser(),
- url::Origin::Create(url),
+ net::IsolationInfo(), {}, process_id, url::Origin::Create(url),
/*client_security_state=*/nullptr, /*options=*/0,
TRAFFIC_ANNOTATION_FOR_TESTS, std::move(handshake_client),
mojo::NullRemote(), mojo::NullRemote(), mojo::NullRemote(),
/*throttling_profile_id=*/std::nullopt,
network::GetTestNetworkRestrictionsId());
+ return stub_handshake_clients_.back().get();
}
protected:
@@ -196,6 +221,75 @@
}
}
+bool ContainsHeader(const mojom::WebSocketHandshakeRequestPtr& request,
+ std::string_view name) {
+ return std::ranges::any_of(request->headers, [&](const auto& header) {
+ return base::EqualsCaseInsensitiveASCII(header->name, name);
+ });
+}
+
+class WebSocketFactoryHandshakeTest : public WebSocketFactoryTest {
+ public:
+ WebSocketFactoryHandshakeTest() {
+ net::test_server::RegisterWebSocketHandler<
+ net::test_server::WebSocketEchoHandler>(&test_server_, "/echo");
+ EXPECT_TRUE(test_server_.Start());
+ }
+
+ void AddBasicAuthCacheEntry() {
+ net::HttpAuthCache* cache = network_context_->url_request_context()
+ ->http_transaction_factory()
+ ->GetSession()
+ ->http_auth_cache();
+ cache->Add(url::SchemeHostPort(test_server_.base_url()),
+ net::HttpAuth::AUTH_SERVER, "TestRealm",
+ net::HttpAuth::AUTH_SCHEME_BASIC, net::NetworkAnonymizationKey(),
+ "Basic realm=TestRealm",
+ net::AuthCredentials(u"user", u"password"), "/");
+ }
+
+ protected:
+ net::EmbeddedTestServer test_server_;
+};
+
+// Verifies that the handshake request reported to a client with raw headers
+// access includes credential headers attached by the network stack.
+TEST_F(WebSocketFactoryHandshakeTest,
+ OpeningHandshakeReportsAuthHeadersWithRawHeadersAccess) {
+ AddBasicAuthCacheEntry();
+
+ StubWebSocketHandshakeClient* client = CreateWebSocket(
+ net::test_server::GetWebSocketURL(test_server_, "/echo"), {});
+
+ mojom::WebSocketHandshakeRequestPtr request =
+ client->WaitForOpeningHandshake();
+ ASSERT_TRUE(request);
+ EXPECT_TRUE(ContainsHeader(request, net::HttpRequestHeaders::kAuthorization));
+}
+
+// Verifies that the handshake request reported to a client without raw
+// headers access omits credential headers attached by the network stack.
+TEST_F(WebSocketFactoryHandshakeTest,
+ OpeningHandshakeOmitsAuthHeadersWithoutRawHeadersAccess) {
+ AddBasicAuthCacheEntry();
+
+ StubWebSocketHandshakeClient* client =
+ CreateWebSocket(net::test_server::GetWebSocketURL(test_server_, "/echo"),
+ {}, OriginatingProcessId::renderer(RendererProcessId(1)));
+
+ mojom::WebSocketHandshakeRequestPtr request =
+ client->WaitForOpeningHandshake();
+ ASSERT_TRUE(request);
+ // The handshake should still report non-credential headers such as Host.
+ EXPECT_TRUE(ContainsHeader(request, net::HttpRequestHeaders::kHost));
+ EXPECT_FALSE(
+ ContainsHeader(request, net::HttpRequestHeaders::kAuthorization));
+ EXPECT_FALSE(
+ ContainsHeader(request, net::HttpRequestHeaders::kProxyAuthorization));
+ EXPECT_EQ(request->headers_text.find(net::HttpRequestHeaders::kAuthorization),
+ std::string::npos);
+}
+
} // namespace
} // namespace network
Original Bug Report
Information Leak: WebSocket handshake leaks HTTP Auth credentials to renderer
Flapjack, 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The Network Service fails to redact sensitive HTTP authentication headers when reporting WebSocket handshake details back to the renderer process. A compromised renderer can exploit this to steal cached user credentials by initiating a WebSocket connection to an authenticated origin or proxy.
Affected files:
services/network/websocket.cc
Estimated timestamp from git blame: 2018-11-30
Vulnerability Description
A potential high-severity information leak exists in Chromium’s Network Service. When a WebSocket connection is initiated, the underlying HTTP transaction (net::HttpNetworkTransaction) automatically appends cached credentials (e.g., for Basic or Digest authentication) to the outgoing request via Authorization and Proxy-Authorization headers if the user has previously authenticated to the target server or proxy.
To support diagnostic features, the Network Service reports the over-the-wire headers back to the renderer process through the mojom::WebSocketHandshakeClient::OnOpeningHandshakeStarted Mojo interface. The code responsible for preparing this message is WebSocketEventHandler::OnStartOpeningHandshake within services/network/websocket.cc.
While the implementation explicitly filters out the Cookie header for renderers that lack raw header access (impl_->has_raw_headers_access_ is false), it fails to apply this filtering to Authorization or Proxy-Authorization headers. Consequently, sensitive cached credentials are inadvertently leaked across the process boundary back to the unprivileged renderer.
Potential Exploitation Steps
An attacker who has achieved code execution within a sandboxed renderer process (e.g., via a V8 or Blink vulnerability) could potentially exploit this to steal user credentials:
- The attacker’s compromised renderer initiates a new WebSocket connection via the
network::mojom::NetworkContext::CreateWebSocketMojo interface, targeting a URL where the user has an active HTTP Auth session (or a request that traverses an authenticated proxy). - The Network Service sets up the connection. Because valid credentials exist in
net::HttpAuthCache,net::HttpNetworkTransaction::BuildRequestHeadersappends theAuthorizationorProxy-Authorizationheader to the request. - The
net::WebSocketBasicHandshakeStreamcopies these enriched headers into aWebSocketHandshakeRequestInfoobject and triggers theOnStartOpeningHandshakeevent. - In
services/network/websocket.cc, the sanitization loop fails to strip the authentication headers because it only checks fornet::HttpRequestHeaders::kCookie. - The Network Service sends the unredacted headers back to the renderer via the
OnOpeningHandshakeStartedIPC. - The attacker’s custom
WebSocketHandshakeClientreceives the IPC and extracts the base64-encoded or plaintext credentials from theAuthorizationheader.
Note: Our tooling agent cannot run code, so these are potential steps derived from static analysis of the codebase.
Impact
This bypasses the renderer/network privilege separation intended to protect credentials. While server authentication credentials (AUTH_SERVER) are partitioned by NetworkAnonymizationKey, proxy credentials (AUTH_PROXY) are not partitioned (they use an empty NAK). Thus, a single compromised renderer could potentially extract global proxy credentials, elevating the impact of the leak.
Recommended Fix
The filtering logic in services/network/websocket.cc within WebSocketEventHandler::OnStartOpeningHandshake should be updated to strip net::HttpRequestHeaders::kAuthorization and net::HttpRequestHeaders::kProxyAuthorization (in addition to kCookie) when impl_->has_raw_headers_access_ is false.
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
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.