Medium watch CVSS 5.3 chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Network
DescriptionInformation leak in Network
ComponentNetwork
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker522823211
Fix commitcc8758dc9c6b (chromium/src) +105/-7
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-11 (per vendor advisory)
Fix landed2026-07-01 (public git)
Disclosed2026-08-25
Patch gap55 days — fix public in source before the advisory

Background

WebSocket opening handshake
the HTTP-upgrade request the network service sends to establish a WebSocket, whose over-the-wire headers can be reported back to the initiating client.
`OnStartOpeningHandshake` / `OnOpeningHandshakeStarted`
the network-service path that packages the outgoing handshake request headers into a mojom::WebSocketHandshakeRequest and delivers them to the WebSocketHandshakeClient.
Raw-headers access (`has_raw_headers_access_`)
a per-client privilege indicating whether the client (e.g. a trusted browser-side consumer) is allowed to see sensitive raw request headers; renderers generally lack it.
`HttpAuthCache`
the network stack’s cache of credentials that it may automatically attach to a request as Authorization / Proxy-Authorization headers without the initiating renderer’s knowledge.

Root Cause Analysis

When reporting the outgoing handshake request in websocket.cc, the code iterates the request headers and, for clients lacking has_raw_headers_access_, skipped only the Cookie header before copying each remaining header into the reported mojom::HttpHeader list. However, the network stack may attach Authorization and Proxy-Authorization headers pulled from HttpAuthCache that the initiating renderer never supplied and should not observe. Because the filter matched only net::HttpRequestHeaders::kCookie, these credential headers were reported back to a non-privileged client, disclosing cached credentials.

The fix widens the skip condition so that, when !has_raw_headers_access_, the loop also continues for headers case-insensitively equal to kAuthorization or kProxyAuthorization, matching the existing treatment of kCookie.

The added tests confirm a raw-headers client still sees Authorization while a renderer client sees Host but neither Authorization nor Proxy-Authorization (including in headers_text).

Key insight
The credential-header filter for non-privileged clients covered only Cookie and omitted Authorization / Proxy-Authorization, which the network stack can silently add from HttpAuthCache; the fix extends the same continue-skip to those two headers.

Attack Path

  1. Obtain cached credentials Ensure the network service’s HttpAuthCache holds credentials (e.g. Basic auth) for a target origin, as modeled by the test’s AddBasicAuthCacheEntry().
  2. Initiate a WebSocket from a renderer A renderer without raw-headers access opens a WebSocket to that origin, causing the network stack to auto-attach Authorization (and/or Proxy-Authorization) from the cache.
  3. Receive the reported handshake The network service reports the over-the-wire handshake request to the client via the opening-handshake callback; pre-patch it included the auto-attached credential headers because only Cookie was filtered.
  4. Read leaked credentials The renderer inspects the reported mojom::WebSocketHandshakeRequest headers and recovers credentials it was never entitled to see.

Impact Assessment

A renderer (or other non-raw-headers client) could learn Authorization/Proxy-Authorization credentials that the network stack attached from the shared HttpAuthCache, an information disclosure in the Network service. Preconditions are that credentials for the target are cached and the client lacks has_raw_headers_access_; the advisory classifies this as an information leak and reports no exploited-in-the-wild status. No memory-safety or code-execution impact is implied.

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

Audit Directions

  • Partial sensitive-header filters
    Search for header-sanitization loops that special-case only Cookie and confirm they also cover Authorization, Proxy-Authorization, and other credential-bearing headers.
  • Auto-attached vs. author-supplied headers
    Review any path that echoes request/response headers to a less-privileged consumer, checking for headers the network stack injects (auth cache, proxy auth, client certs) that the consumer never set.
  • Raw-headers gating consistency
    Audit all has_raw_headers_access_ branches across the network service to ensure the same set of sensitive headers is filtered uniformly, including in serialized headers_text blobs.
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)));
+
+  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…

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