Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Network
DescriptionInformation leak in Network
ComponentNetwork
Bug ClassLogic Error
Tracker522823211
Fix commitcc8758dc9c6b (chromium/src) +105/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
WebSocketFactoryHandshakeTest
services/network/websocket_factory_unittest.cc
modified
WebSocketFactoryHandshakeTest
services/network/websocket_factory_unittest.cc
modified

Files Changed

  • services/network/websocket.cc
  • services/network/websocket_factory_unittest.cc
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)));
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by rj...@google.com

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:

  1. The attacker’s compromised renderer initiates a new WebSocket connection via the network::mojom::NetworkContext::CreateWebSocket Mojo interface, targeting a URL where the user has an active HTTP Auth session (or a request that traverses an authenticated proxy).
  2. The Network Service sets up the connection. Because valid credentials exist in net::HttpAuthCache, net::HttpNetworkTransaction::BuildRequestHeaders appends the Authorization or Proxy-Authorization header to the request.
  3. The net::WebSocketBasicHandshakeStream copies these enriched headers into a WebSocketHandshakeRequestInfo object and triggers the OnStartOpeningHandshake event.
  4. In services/network/websocket.cc, the sanitization loop fails to strip the authentication headers because it only checks for net::HttpRequestHeaders::kCookie.
  5. The Network Service sends the unredacted headers back to the renderer via the OnOpeningHandshakeStarted IPC.
  6. The attacker’s custom WebSocketHandshakeClient receives the IPC and extracts the base64-encoded or plaintext credentials from the Authorization header.

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.

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.

View on issue tracker