Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in WebRTC
DescriptionImproper input validation in WebRTC
ComponentWebRTC
Bug ClassLogic Error
Tracker511794959
Fix commitc4ef6a270b01 (chromium/src) +133/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
ProxyResolvingClientSocketFactory
services/network/p2p/socket.h
modified
P2PMessageThrottler
services/network/p2p/socket.h
modified
COMPONENT_EXPORT
services/network/p2p/socket.h
modified
if
services/network/p2p/socket_manager.cc
modified
TEST_F
services/network/p2p/socket_manager_unittest.cc
modified
if
services/network/p2p/socket_tcp.cc
modified

Files Changed

  • services/network/p2p/socket.cc
  • services/network/p2p/socket.h
  • services/network/p2p/socket_manager.cc
  • services/network/p2p/socket_manager_unittest.cc
  • services/network/p2p/socket_tcp.cc
From c4ef6a270b016c86dd63549090833fff6f2f9ffc Mon Sep 17 00:00:00 2001
From: Guido Urdaneta <guidou@chromium.org>
Date: Mon, 06 Jul 2026 16:13:17 -0700
Subject: [PATCH] Enforce scheme and IP endpoint port restrictions on P2P sockets

Validate remote target ports in the network service when creating or
using P2P sockets.

Add explicit verification against net::IsPortAllowedForScheme() (for
scheme "stun") and net::IsPortAllowedForIpEndpoint() in
P2PSocketManager::CreateSocket(), P2PSocketTcpBase::Init(),
P2PSocketUdp::Init(), and P2PSocketUdp::DoSend(). Reject socket creation
or destroy the socket if a restricted target port is specified.

Fixed: 511794959
Change-Id: I4923268c4c5bb73151cb4bf1cf871660fb4b1235
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8032493
Reviewed-by: Danil Chapovalov <danilchap@chromium.org>
Commit-Queue: Guido Urdaneta <guidou@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1657556}
---

diff --git a/services/network/p2p/socket.cc b/services/network/p2p/socket.cc
index f272971..1c18e75 100644
--- a/services/network/p2p/socket.cc
+++ b/services/network/p2p/socket.cc
@@ -7,6 +7,7 @@
 #include <utility>
 
 #include "base/containers/span_reader.h"
+#include "base/feature.h"
 #include "base/functional/bind.h"
 #include "base/metrics/histogram_macros.h"
 #include "net/base/net_errors.h"
@@ -59,6 +60,9 @@
 
 namespace network {
 
+BASE_FEATURE(kEnforceP2PSocketPortRestrictions,
+             base::FEATURE_ENABLED_BY_DEFAULT);
+
 P2PSocket::P2PSocket(Delegate* delegate,
                      mojo::PendingRemote<mojom::P2PSocketClient> client,
                      mojo::PendingReceiver<mojom::P2PSocket> socket,
diff --git a/services/network/p2p/socket.h b/services/network/p2p/socket.h
index 2238fb2..4c0398c 100644
--- a/services/network/p2p/socket.h
+++ b/services/network/p2p/socket.h
@@ -12,6 +12,7 @@
 
 #include "base/component_export.h"
 #include "base/containers/span.h"
+#include "base/feature.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
 #include "base/unguessable_token.h"
@@ -35,6 +36,9 @@
 class ProxyResolvingClientSocketFactory;
 class P2PMessageThrottler;
 
+COMPONENT_EXPORT(NETWORK_SERVICE)
+BASE_DECLARE_FEATURE(kEnforceP2PSocketPortRestrictions);
+
 // Base class for P2P sockets.
 class COMPONENT_EXPORT(NETWORK_SERVICE) P2PSocket : public mojom::P2PSocket {
  public:
diff --git a/services/network/p2p/socket_manager.cc b/services/network/p2p/socket_manager.cc
index 9e45f39..3a814c1 100644
--- a/services/network/p2p/socket_manager.cc
+++ b/services/network/p2p/socket_manager.cc
@@ -9,6 +9,7 @@
 #include <optional>
 #include <utility>
 
+#include "base/feature_list.h"
 #include "base/functional/bind.h"
 #include "base/memory/raw_ptr.h"
 #include "base/task/single_thread_task_runner.h"
@@ -19,6 +20,7 @@
 #include "net/base/network_anonymization_key.h"
 #include "net/base/network_handle.h"
 #include "net/base/network_interfaces.h"
+#include "net/base/port_util.h"
 #include "net/base/sys_addrinfo.h"
 #include "net/dns/dns_util.h"
 #include "net/dns/host_resolver.h"
@@ -415,6 +417,20 @@
     return;
   }
 
+  if (base::FeatureList::IsEnabled(kEnforceP2PSocketPortRestrictions)) {
+    // When creating UDP sockets, the renderer initially passes port 0.
+    // Port 0 is normally restricted, so we skip validation when type is UDP
+    // and port is 0.
+    bool should_skip_port_validation =
+        type == P2P_SOCKET_UDP && remote_address.ip_address.port() == 0;
+    bool is_restricted_port =
+        !net::IsPortAllowedForIpEndpoint(remote_address.ip_address) ||
+        !net::IsPortAllowedForScheme(remote_address.ip_address.port(), "stun");
+    if (is_restricted_port && !should_skip_port_validation) {
+      return;
+    }
+  }
+
   if (!proxy_resolving_socket_factory_) {
     proxy_resolving_socket_factory_ =
         std::make_unique<ProxyResolvingClientSocketFactory>(
diff --git a/services/network/p2p/socket_manager_unittest.cc b/services/network/p2p/socket_manager_unittest.cc
index 770679d..9153df2e 100644
--- a/services/network/p2p/socket_manager_unittest.cc
+++ b/services/network/p2p/socket_manager_unittest.cc
@@ -12,15 +12,18 @@
 #include "base/test/task_environment.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
 #include "net/base/features.h"
 #include "net/log/net_log_with_source.h"
 #include "net/socket/datagram_client_socket.h"
 #include "net/socket/socket_test_util.h"
 #include "net/socket/stream_socket.h"
 #include "net/test/test_with_task_environment.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
 #include "net/url_request/url_request_context.h"
 #include "net/url_request/url_request_context_builder.h"
 #include "net/url_request/url_request_test_util.h"
+#include "services/network/p2p/socket.h"
 #include "services/network/p2p/socket_test_utils.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -90,4 +93,38 @@
   EXPECT_TRUE(fake_notification_client->get_network_list_changed());
 }
 
+TEST_F(P2PSocketManagerTest, CreateSocketRestrictedPort) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(kEnforceP2PSocketPortRestrictions);
+  SetUpSocketManager();
+
+  mojo::Remote<mojom::P2PSocket> socket_remote;
+  mojo::PendingRemote<mojom::P2PSocketClient> client_remote;
+  std::ignore = client_remote.InitWithNewPipeAndPassReceiver();
+
+  P2PPortRange port_range;
+  port_range.min_port = 0;
+  port_range.max_port = 0;
+
+  P2PHostAndIPEndPoint remote_address;
+  net::IPAddress ip(192, 168, 1, 10);
+  remote_address.ip_address =
+      net::IPEndPoint(ip, 25);  // Restricted SMTP port (25).
+
+  mojo::PendingReceiver<mojom::P2PSocket> socket_receiver =
+      socket_remote.BindNewPipeAndPassReceiver();
+  base::RunLoop run_loop;
+  socket_remote.set_disconnect_handler(run_loop.QuitClosure());
+
+  socket_manager_remote_->CreateSocket(
+      P2P_SOCKET_STUN_TCP_CLIENT,
+      net::IPEndPoint(net::IPAddress::IPv4Localhost(), 0), port_range,
+      remote_address,
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS),
+      std::nullopt, std::move(client_remote), std::move(socket_receiver));
+
+  run_loop.Run();
+  EXPECT_FALSE(socket_remote.is_connected());
+}
+
 }  // namespace network
diff --git a/services/network/p2p/socket_tcp.cc b/services/network/p2p/socket_tcp.cc
index a9ae16a..e9c6035 100644
--- a/services/network/p2p/socket_tcp.cc
+++ b/services/network/p2p/socket_tcp.cc
@@ -11,6 +11,7 @@
 
 #include "base/containers/span.h"
 #include "base/containers/span_writer.h"
+#include "base/feature_list.h"
 #include "base/functional/bind.h"
 #include "base/numerics/byte_conversions.h"
 #include "base/time/time.h"
@@ -18,6 +19,7 @@
 #include "net/base/io_buffer.h"
 #include "net/base/net_errors.h"
 #include "net/base/network_anonymization_key.h"
+#include "net/base/port_util.h"
 #include "net/socket/client_socket_factory.h"
 #include "net/socket/client_socket_handle.h"
 #include "net/socket/ssl_client_socket.h"
@@ -82,6 +84,16 @@
 
   remote_address_ = remote_address;
 
+  if (base::FeatureList::IsEnabled(kEnforceP2PSocketPortRestrictions)) {
+    bool is_restricted_port =
+        !net::IsPortAllowedForIpEndpoint(remote_address.ip_address) ||
+        !net::IsPortAllowedForScheme(remote_address.ip_address.port(), "stun");
+    if (is_restricted_port) {
+      OnError();
+      return;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/network/p2p/socket_manager_unittest.cc b/services/network/p2p/socket_manager_unittest.cc
index 770679d..9153df2e 100644
--- a/services/network/p2p/socket_manager_unittest.cc
+++ b/services/network/p2p/socket_manager_unittest.cc
@@ -12,15 +12,18 @@
 #include "base/test/task_environment.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
 #include "net/base/features.h"
 #include "net/log/net_log_with_source.h"
 #include "net/socket/datagram_client_socket.h"
 #include "net/socket/socket_test_util.h"
 #include "net/socket/stream_socket.h"
 #include "net/test/test_with_task_environment.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
 #include "net/url_request/url_request_context.h"
 #include "net/url_request/url_request_context_builder.h"
 #include "net/url_request/url_request_test_util.h"
+#include "services/network/p2p/socket.h"
 #include "services/network/p2p/socket_test_utils.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -90,4 +93,38 @@
   EXPECT_TRUE(fake_notification_client->get_network_list_changed());
 }
 
+TEST_F(P2PSocketManagerTest, CreateSocketRestrictedPort) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(kEnforceP2PSocketPortRestrictions);
+  SetUpSocketManager();
+
+  mojo::Remote<mojom::P2PSocket> socket_remote;
+  mojo::PendingRemote<mojom::P2PSocketClient> client_remote;
+  std::ignore = client_remote.InitWithNewPipeAndPassReceiver();
+
+  P2PPortRange port_range;
+  port_range.min_port = 0;
+  port_range.max_port = 0;
+
+  P2PHostAndIPEndPoint remote_address;
+  net::IPAddress ip(192, 168, 1, 10);
+  remote_address.ip_address =
+      net::IPEndPoint(ip, 25);  // Restricted SMTP port (25).
+
+  mojo::PendingReceiver<mojom::P2PSocket> socket_receiver =
+      socket_remote.BindNewPipeAndPassReceiver();
+  base::RunLoop run_loop;
+  socket_remote.set_disconnect_handler(run_loop.QuitClosure());
+
+  socket_manager_remote_->CreateSocket(
+      P2P_SOCKET_STUN_TCP_CLIENT,
+      net::IPEndPoint(net::IPAddress::IPv4Localhost(), 0), port_range,
+      remote_address,
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS),
+      std::nullopt, std::move(client_remote), std::move(socket_receiver));
+
+  run_loop.Run();
+  EXPECT_FALSE(socket_remote.is_connected());
+}
+
 }  // namespace network
diff --git a/services/network/p2p/socket_tcp_unittest.cc b/services/network/p2p/socket_tcp_unittest.cc
index eb0153e4..eef56fb 100644
--- a/services/network/p2p/socket_tcp_unittest.cc
+++ b/services/network/p2p/socket_tcp_unittest.cc
@@ -763,4 +763,38 @@
   base::RunLoop().RunUntilIdle();
 }
 
+TEST_F(P2PSocketTcpTest, InitRejectsRestrictedPort) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(kEnforceP2PSocketPortRestrictions);
+  mojo::PendingRemote<mojom::P2PSocketClient> socket_client;
+  mojo::PendingRemote<mojom::P2PSocket> socket;
+  mojo::PendingReceiver<mojom::P2PSocket> socket_receiver =
+      socket.InitWithNewPipeAndPassReceiver();
+
+  FakeSocketClient fake_client(std::move(socket),
+                               socket_client.InitWithNewPipeAndPassReceiver());
+
+  net::MockClientSocketFactory mock_socket_factory;
+  std::unique_ptr<net::URLRequestContextBuilder> context_builder =
+      net::CreateTestURLRequestContextBuilder();
+  context_builder->set_client_socket_factory_for_testing(&mock_socket_factory);
+  std::unique_ptr<net::URLRequestContext> context = context_builder->Build();
+  ProxyResolvingClientSocketFactory factory(context.get());
+
+  FakeP2PSocketDelegate socket_delegate;
+  auto host = std::make_unique<P2PSocketTcp>(
+      &socket_delegate, std::move(socket_client), std::move(socket_receiver),
+      P2P_SOCKET_TCP_CLIENT, TRAFFIC_ANNOTATION_FOR_TESTS, &factory);
+
+  P2PSocketTcp* host_ptr = host.get();
+  socket_delegate.ExpectDestruction(std::move(host));
+
+  P2PHostAndIPEndPoint dest;
+  dest.ip_address = ParseAddress(kTestIpAddress1, 25);
+  host_ptr->Init(net::IPEndPoint(net::IPAddress::IPv4Localhost(), 0), 0, 0,
+                 dest, net::NetworkAnonymizationKey());
+
+  base::RunLoop().RunUntilIdle();
+}
+
 }  // namespace network
diff --git a/services/network/p2p/socket_udp_unittest.cc b/services/network/p2p/socket_udp_unittest.cc
index 4dfd6c6..aab14c84 100644
--- a/services/network/p2p/socket_udp_unittest.cc
+++ b/services/network/p2p/socket_udp_unittest.cc
@@ -2033,4 +2033,21 @@
   histograms.ExpectTotalCount("WebRTC.P2P.UDP.SendRetryDelay", 0);
 }
 
+TEST_F(P2PSocketUdpTest, SendRejectsRestrictedPort) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(kEnforceP2PSocketPortRestrictions);
+  std::vector<uint8_t> request_packet;
+  CreateStunRequest(&request_packet);
+
+  net::IPEndPoint restricted_dest = ParseAddress(kTestIpAddress1, 25);
+
+  socket_ = nullptr;
+  P2PSocketUdp* socket_impl_ptr = socket_impl_.get();
+  socket_delegate_.ExpectDestruction(std::move(socket_impl_));
+  socket_impl_ptr->Send(request_packet, P2PPacketInfo(restricted_dest, {}, 0));
+
+  EXPECT_TRUE(
+      base::test::RunUntil([&]() { return fake_client_->connection_error(); }));
+}
+
 }  // namespace network
Loading diff…

Original Bug Report

reported by vm...@google.com

Restricted port bypass and Cross-Protocol SSRF via P2PSocketManager

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 compromised renderer can bypass restricted port checks (e.g., for SMTP port 25) by calling network::mojom::P2PSocketManager::CreateSocket directly via Mojo, as the network process lacks sufficient port validation for non-loopback addresses. By using a STUN TCP socket, the attacker can leverage flaws in STUN packet parsing and framing to keep the connection alive and smuggle text-based protocol commands. This enables potential Cross-Protocol SSRF attacks against internal network services.

Affected files:

  • services/network/p2p/socket_manager.cc
  • services/network/p2p/socket_tcp.cc
  • services/network/p2p/socket.cc

Estimated timestamp from git blame: 2024-11-18

Summary

A vulnerability in P2PSocketManager::CreateSocket allows a compromised renderer process to bypass Blink’s port restrictions and establish TCP connections to restricted ports (such as 25, 6667) on internal network addresses. Combined with shallow STUN packet validation and a parsing flaw in how the network service handles non-STUN server greetings, an attacker can maintain the connection and smuggle arbitrary text protocol commands, resulting in a Cross-Protocol Server-Side Request Forgery (SSRF).

Technical Details

1. Bypassing Port Restrictions via Mojo

Normally, when a WebRTC application attempts to connect to a restricted port (e.g., SMTP port 25), the connection is blocked in the renderer by IpcPacketSocketFactory::CreateClientTcpSocket using net::IsPortAllowedForScheme. However, a compromised renderer can bypass this check by directly obtaining the network::mojom::P2PSocketManager interface via the BrowserInterfaceBroker and calling CreateSocket.

In the network service, P2PSocketManager::CreateSocket (services/network/p2p/socket_manager.cc) validates the local port range but does not validate the remote_address against the restricted port list. It initializes a P2PSocketTcpBase which uses ProxyResolvingClientSocketFactory. The underlying TCPClientSocket only checks net::IsPortAllowedForIpEndpoint, which currently only blocks restricted ports on the loopback address (127.0.0.1), allowing connections to internal network IPs (e.g., 192.168.1.10:25).

2. Connection Teardown Bypass

When connecting to a text-based server like SMTP, the server sends a greeting (e.g., "220 smtp.example.com\r\n"). The network process processes this in P2PSocketStunTcp::ProcessInput, which calls GetExpectedPacketSize.

GetExpectedPacketSize incorrectly parses the ASCII string "220 " (hex 0x3232 0x3020). It treats 0x3232 as a STUN message type (top two bits are zero) and 0x3020 as a length of 12,320 bytes. Expecting a massive STUN packet, ProcessInput buffers the data indefinitely instead of passing it to OnPacket. Because OnPacket is never called, the check that would normally terminate the socket upon receiving non-STUN data is bypassed, keeping the TCP connection alive.

3. Protocol Smuggling

With the connection alive but formally “unconnected” in STUN state, outgoing packets must pass P2PSocket::GetStunPacketType. This function only validates the first 8 bytes (Type, Length, and Magic Cookie).

The attacker can craft a payload starting with a valid 8-byte STUN header, followed by a 12-byte Transaction ID and plain-text SMTP commands (e.g., \r\nHELO attacker.com\r\nMAIL FROM:...). Because P2PSocketStunTcp::DoSend writes the STUN packet directly to the TCP stream without an RFC 4571 length prefix, the SMTP server receives the raw bytes. It treats the 20-byte STUN header as an unrecognized command (often ignoring it without closing the connection) and successfully executes the subsequent smuggled commands.

Potential Steps to Reproduce

Note: Our tooling cannot execute code, so these are suggested steps based on static analysis.

  1. From a compromised renderer, use BrowserInterfaceBroker to request network::mojom::P2PSocketManager.
  2. Call CreateSocket with type=P2P_SOCKET_STUN_TCP_CLIENT and remote_address pointing to an internal SMTP server (e.g., 192.168.1.10:25).
  3. Wait for the SocketCreated Mojo callback, confirming the TCP connection.
  4. Send a crafted STUN packet via P2PSocketClient. The packet must contain a valid 8-byte STUN header where the length field accurately accounts for the entire packet size, followed by the smuggled SMTP commands.
  5. The network process will transmit the payload. The SMTP server will buffer the 20-byte STUN header, hit the newline, process it as an invalid command, and then process the smuggled commands.

Suggested Fix

Add a net::IsPortAllowedForScheme(remote_address.ip_address.port(), "stun") check within P2PSocketManager::CreateSocket (or deeper in P2PSocketTcpBase::Init / P2PSocketUdp::Init) in the network service to ensure that compromised renderers cannot request P2P connections to restricted ports on any IP address.

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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