Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in WebSockets
DescriptionInsufficient validation of untrusted input in WebSockets
ComponentWebSockets
Bug ClassLogic Error
Tracker512999037
Fix commitc5cad7b6ce9d (chromium/src) +238/-59
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
content/browser/websockets/websocket_connector_impl.cc
modified
GURL
services/network/public/cpp/websocket_utils.h
modified
IsolationInfo
services/network/public/cpp/websocket_utils.h
modified

Files Changed

  • content/browser/websockets/websocket_connector_impl.cc
  • services/network/public/cpp/BUILD.gn
  • services/network/public/cpp/websocket_utils.cc
  • services/network/public/cpp/websocket_utils.h
From c5cad7b6ce9d16f79de159da9d7312c7f9bc440b Mon Sep 17 00:00:00 2001
From: Adam Rice <ricea@chromium.org>
Date: Mon, 08 Jun 2026 02:22:43 -0700
Subject: [PATCH] Validate WebSocket connect parameters consistently

This change introduces centralized validation for WebSocket connection
parameters to prevent potential security vulnerabilities and ensure
consistency across the browser and network processes.

- Added `services/network/public/cpp/websocket_utils.cc/h` for shared
  validation logic.
- Updated `WebSocketConnectorImpl` to use the new validation.
- Updated `WebSocketFactory` to use the new implementation.

Bug: 512999037
Change-Id: Idd67b4f74319612e3664034eb719fb6ebd58d305
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7874387
Reviewed-by: Nidhi Jaju <nidhijaju@chromium.org>
Commit-Queue: Adam Rice <ricea@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1643052}
---

diff --git a/content/browser/websockets/websocket_connector_impl.cc b/content/browser/websockets/websocket_connector_impl.cc
index 107cf4c..558561c 100644
--- a/content/browser/websockets/websocket_connector_impl.cc
+++ b/content/browser/websockets/websocket_connector_impl.cc
@@ -15,8 +15,10 @@
 #include "content/public/common/child_process_id_util.h"
 #include "content/public/common/content_client.h"
 #include "content/public/common/content_switches.h"
+#include "mojo/public/cpp/bindings/message.h"
 #include "net/storage_access_api/status.h"
 #include "services/network/public/cpp/features.h"
+#include "services/network/public/cpp/websocket_utils.h"
 #include "url/gurl.h"
 
 namespace content {
@@ -84,6 +86,13 @@
         handshake_client,
     const std::optional<base::UnguessableToken>& throttling_profile_id) {
   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+
+  if (auto error = network::VerifyWebSocketConnectParameters(
+          url, requested_protocols, isolation_info_)) {
+    mojo::ReportBadMessage(*error);
+    return;
+  }
+
   RenderProcessHost* process = RenderProcessHost::FromID(frame_id_.child_id);
   if (!process) {
     return;
diff --git a/services/network/public/cpp/BUILD.gn b/services/network/public/cpp/BUILD.gn
index 78f9387..8d5aa0c 100644
--- a/services/network/public/cpp/BUILD.gn
+++ b/services/network/public/cpp/BUILD.gn
@@ -175,6 +175,8 @@
     "url_util.h",
     "weak_wrapper_shared_url_loader_factory.cc",
     "weak_wrapper_shared_url_loader_factory.h",
+    "websocket_utils.cc",
+    "websocket_utils.h",
     "wrapper_shared_url_loader_factory.cc",
     "wrapper_shared_url_loader_factory.h",
     "x_frame_options_parser.cc",
@@ -914,6 +916,7 @@
     "url_request_mojom_traits_unittest.cc",
     "url_util_unittest.cc",
     "web_sandbox_flags_unittest.cc",
+    "websocket_utils_unittest.cc",
     "x_frame_options_parser_unittest.cc",
   ]
 
diff --git a/services/network/public/cpp/websocket_utils.cc b/services/network/public/cpp/websocket_utils.cc
new file mode 100644
index 0000000..a1fb38e7
--- /dev/null
+++ b/services/network/public/cpp/websocket_utils.cc
@@ -0,0 +1,82 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/network/public/cpp/websocket_utils.h"
+
+#include <algorithm>
+#include <optional>
+#include <ranges>
+#include <string_view>
+
+#include "net/base/isolation_info.h"
+#include "url/gurl.h"
+
+namespace network {
+
+namespace {
+
+bool IsValidSubprotocolCharacter(char character) {
+  constexpr auto kMinimumProtocolCharacter = '!';  // U+0021.
+  constexpr auto kMaximumProtocolCharacter = '~';  // U+007E.
+  // Set to true if character does not matches "separators" ABNF defined in
+  // RFC2616. SP and HT are excluded since the range check excludes them.
+  const bool is_separator =
+      character == '"' || character == '(' || character == ')' ||
+      character == ',' || character == '/' ||
+      (character >= ':' &&
+       character <=
+           '@')  // U+003A - U+0040 (':', ';', '<', '=', '>', '?', '@').
+      || (character >= '[' &&
+          character <= ']')  // U+005B - U+005D ('[', '\\', ']').
+      || character == '{' || character == '}';
+  return character >= kMinimumProtocolCharacter &&
+         character <= kMaximumProtocolCharacter && !is_separator;
+}
+
+bool IsValidSubprotocolString(const std::string& protocol) {
+  if (protocol.empty()) {
+    return false;
+  }
+  return std::ranges::all_of(protocol, IsValidSubprotocolCharacter);
+}
+
+bool IsValidProtocols(const std::vector<std::string>& requested_protocols) {
+  // Fail if not all elements in |protocols| are valid.
+  if (!std::ranges::all_of(requested_protocols, IsValidSubprotocolString)) {
+    return false;
+  }
+
+  // Fail if there're duplicated elements in |protocols|.
+  std::vector<std::string_view> protocols(std::from_range, requested_protocols);
+  std::ranges::sort(protocols);
+  if (std::ranges::adjacent_find(protocols) != protocols.end()) {
+    return false;
+  }
+
+  return true;
+}
+
+}  // namespace
+
+std::optional<std::string> VerifyWebSocketConnectParameters(
+    const GURL& url,
+    const std::vector<std::string>& requested_protocols,
+    const net::IsolationInfo& isolation_info) {
+  if (isolation_info.request_type() !=
+      net::IsolationInfo::RequestType::kOther) {
+    return "WebSocket's IsolationInfo::RequestType must be kOther";
+  }
+
+  if (!url.SchemeIsWSOrWSS()) {
+    return "Invalid scheme.";
+  }
+
+  if (!IsValidProtocols(requested_protocols)) {
+    return "Invalid protocols.";
+  }
+
+  return std::nullopt;
+}
+
+}  // namespace network
diff --git a/services/network/public/cpp/websocket_utils.h b/services/network/public/cpp/websocket_utils.h
new file mode 100644
index 0000000..5cc30e2
--- /dev/null
+++ b/services/network/public/cpp/websocket_utils.h
@@ -0,0 +1,32 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_NETWORK_PUBLIC_CPP_WEBSOCKET_UTILS_H_
+#define SERVICES_NETWORK_PUBLIC_CPP_WEBSOCKET_UTILS_H_
+
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "base/component_export.h"
+
+class GURL;
+
+namespace net {
+class IsolationInfo;
+}  // namespace net
+
+namespace network {
+
+// Verifies the WebSocket connection parameters. Returns std::nullopt if they
+// are valid. If not, returns the error message.
+COMPONENT_EXPORT(NETWORK_CPP)
+std::optional<std::string> VerifyWebSocketConnectParameters(
+    const GURL& url,
+    const std::vector<std::string>& requested_protocols,
+    const net::IsolationInfo& isolation_info);
+
+}  // namespace network
+
+#endif  // SERVICES_NETWORK_PUBLIC_CPP_WEBSOCKET_UTILS_H_
diff --git a/services/network/public/cpp/websocket_utils_unittest.cc b/services/network/public/cpp/websocket_utils_unittest.cc
new file mode 100644
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/network/public/cpp/websocket_utils_unittest.cc b/services/network/public/cpp/websocket_utils_unittest.cc
new file mode 100644
index 0000000..6f9322c
--- /dev/null
+++ b/services/network/public/cpp/websocket_utils_unittest.cc
@@ -0,0 +1,108 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/network/public/cpp/websocket_utils.h"
+
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "net/base/isolation_info.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace network {
+namespace {
+
+net::IsolationInfo CreateIsolationInfo(
+    net::IsolationInfo::RequestType request_type) {
+  url::Origin origin = url::Origin::Create(GURL("https://example.com"));
+  return net::IsolationInfo::Create(request_type, origin, origin,
+                                    net::SiteForCookies::FromOrigin(origin));
+}
+
+TEST(WebSocketUtilsTest, ValidParameters) {
+  auto isolation_info =
+      CreateIsolationInfo(net::IsolationInfo::RequestType::kOther);
+
+  EXPECT_EQ(std::nullopt, VerifyWebSocketConnectParameters(
+                              GURL("ws://example.com/"), {}, isolation_info));
+
+  EXPECT_EQ(std::nullopt,
+            VerifyWebSocketConnectParameters(GURL("wss://example.com/"),
+                                             {"chat"}, isolation_info));
+
+  EXPECT_EQ(std::nullopt, VerifyWebSocketConnectParameters(
+                              GURL("wss://example.com/"), {"chat", "superchat"},
+                              isolation_info));
+}
+
+TEST(WebSocketUtilsTest, InvalidIsolationInfo) {
+  EXPECT_EQ(
+      "WebSocket's IsolationInfo::RequestType must be kOther",
+      VerifyWebSocketConnectParameters(
+          GURL("ws://example.com/"), {},
+          CreateIsolationInfo(net::IsolationInfo::RequestType::kMainFrame)));
+
+  EXPECT_EQ(
+      "WebSocket's IsolationInfo::RequestType must be kOther",
+      VerifyWebSocketConnectParameters(
+          GURL("ws://example.com/"), {},
+          CreateIsolationInfo(net::IsolationInfo::RequestType::kSubFrame)));
+}
+
+TEST(WebSocketUtilsTest, InvalidScheme) {
+  auto isolation_info =
+      CreateIsolationInfo(net::IsolationInfo::RequestType::kOther);
+
+  const std::string expected_error = "Invalid scheme.";
+  EXPECT_EQ(expected_error,
+            VerifyWebSocketConnectParameters(GURL("http://example.com/"), {},
+                                             isolation_info));
+  EXPECT_EQ(expected_error,
+            VerifyWebSocketConnectParameters(GURL("https://example.com/"), {},
+                                             isolation_info));
+  EXPECT_EQ(expected_error,
+            VerifyWebSocketConnectParameters(GURL("ftp://example.com/"), {},
+                                             isolation_info));
+  EXPECT_EQ(expected_error,
+            VerifyWebSocketConnectParameters(GURL("data:text/plain,Hello"), {},
+                                             isolation_info));
+}
+
+TEST(WebSocketUtilsTest, InvalidProtocols) {
+  auto isolation_info =
+      CreateIsolationInfo(net::IsolationInfo::RequestType::kOther);
+
+  const std::string expected_error = "Invalid protocols.";
+
+  // Duplicates
+  EXPECT_EQ(expected_error,
+            VerifyWebSocketConnectParameters(GURL("ws://example.com/"),
+                                             {"chat", "chat"}, isolation_info));
+
+  // Empty string
+  EXPECT_EQ(expected_error,
+            VerifyWebSocketConnectParameters(GURL("ws://example.com/"), {""},
+                                             isolation_info));
+
+  // Invalid character: space
+  EXPECT_EQ(expected_error,
+            VerifyWebSocketConnectParameters(GURL("ws://example.com/"),
+                                             {"chat room"}, isolation_info));
+
+  // Invalid character: separator
+  EXPECT_EQ(expected_error,
+            VerifyWebSocketConnectParameters(GURL("ws://example.com/"),
+                                             {"chat,room"}, isolation_info));
+
+  // Invalid character: control character
+  EXPECT_EQ(expected_error,
+            VerifyWebSocketConnectParameters(GURL("ws://example.com/"),
+                                             {"chat\x01"}, isolation_info));
+}
+
+}  // namespace
+}  // namespace network
Loading diff…

Original Bug Report

reported by vm...@google.com

WebSocket scheme validation bypass leading to telemetry spoofing and kill evasion

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: WebSocketConnectorImpl in the browser process fails to validate the URL scheme provided by the renderer, allowing non-WebSocket URLs to be processed. This enables a compromised renderer to spoof extension telemetry for arbitrary URLs and evade process termination for malformed Mojo messages.

Affected files:

  • content/browser/websockets/websocket_connector_impl.cc
  • extensions/browser/api/web_request/web_request_proxying_websocket.cc
  • services/network/websocket_factory.cc
  • extensions/browser/api/web_request/web_request_resource_type.cc

Estimated timestamp from git blame: 2019-07-04

Summary

The blink.mojom.WebSocketConnector interface, implemented by WebSocketConnectorImpl in the browser process, fails to validate that the URL provided by the renderer uses a WebSocket scheme (ws or wss). This lack of validation allows a compromised renderer to spoof extension telemetry and evade the ‘bad message’ process termination mechanism, while also causing stability issues in debug builds.

Root Cause

In content/browser/websockets/websocket_connector_impl.cc, the Connect method receives a renderer-controlled GURL but does not verify the scheme before passing it to the browser client or the network context:

// content/browser/websockets/websocket_connector_impl.cc:78
void WebSocketConnectorImpl::Connect(const GURL& url, ...) {
  // ...
  if (GetContentClient()->browser()->WillInterceptWebSocket(frame)) {
    GetContentClient()->browser()->CreateWebSocket(..., url, ...); // No scheme check
    return;
  }
  // ...
  storage_partition->GetNetworkContext()->CreateWebSocket(url, ...); // No scheme check
}

Security Impacts

1. Extension Telemetry Spoofing

When an extension using webRequest or declarativeNetRequest is active, the request is intercepted by WebRequestProxyingWebSocket. Because the URL scheme is not validated, the type mapping logic in extensions/browser/api/web_request/web_request_resource_type.cc defaults to WebRequestResourceType::OTHER for non-WebSocket schemes (like https or file).

This allows a compromised renderer to trigger extension events (like onBeforeRequest or onSendHeaders) for arbitrary URLs. This can be used to inject fake data into security logs or bypass extension rules that are only configured to monitor specific resource types.

2. Defense-in-Depth (Bad-Message Kill) Evasion

If the request is not blocked by an extension, it eventually reaches WebSocketFactory::CreateWebSocket in the Network Service. The Network Service correctly identifies the invalid scheme and reports a bad message via mojo::ReportBadMessage("Invalid scheme.").

However, because the validation happens on the IPC pipe between the browser process and the Network Service, the ‘bad message’ is attributed to the browser process rather than the originating renderer. In official builds, this results in the Network Service logging a warning and calling DumpWithoutCrashing(), allowing the compromised renderer to continue execution and evade the process termination that should have occurred.

3. Stability (Crashes)

In debug builds, this logic flaw causes a browser-wide crash. WebRequestProxyingWebSocket::OnBeforeRequestComplete contains a DCHECK(info_.url.SchemeIsWSOrWSS()) at line 361. Since the browser process did not validate the scheme earlier, this DCHECK fails when a compromised renderer sends a non-WebSocket URL.

Potential Reproduction Steps

  1. Compromise a renderer process (e.g., via a V8 vulnerability).
  2. From the compromised renderer, obtain the frame’s blink.mojom.WebSocketConnector interface.
  3. Call Connect with an arbitrary URL, such as https://accounts.google.com/ or file:///etc/passwd.
  4. Observe that webRequest extensions receive events for the provided URL with a resource type of “other”.
  5. Observe that the renderer is not terminated despite sending an invalid Mojo message.
  6. In debug builds, observe that the entire browser process crashes.

Suggested Fix

Explicitly validate the URL scheme in WebSocketConnectorImpl::Connect and WebSocketConnectorImpl::ConnectCalledByContentBrowserClient. If the URL does not have a ws or wss scheme, the browser process should call mojo::ReportBadMessage to terminate the compromised renderer immediately.

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.

View on issue tracker
Links in the report