Medium watch CVSS 3.1 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in WebRTC
DescriptionImproper input validation in WebRTC
ComponentWebRTC
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker511794959
Fix commitc4ef6a270b01 (chromium/src) +133/-3
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-10 (per vendor advisory)
Fix landed2026-07-06 (public git)
Disclosed2026-08-25
Patch gap50 days — fix public in source before the advisory

Background

P2P sockets
the network-service sockets WebRTC uses for peer-to-peer connectivity (STUN/TURN/ICE), created and driven on behalf of a renderer.
`net::IsPortAllowedForIpEndpoint()`
a net helper that rejects endpoints on ports Chrome blocks (well-known service ports such as SMTP 25, and port 0).
`net::IsPortAllowedForScheme()`
a net helper that rejects ports disallowed for a given scheme; here it is queried with scheme "stun".
`kEnforceP2PSocketPortRestrictions`
a new BASE_FEATURE (enabled by default) that gates the added port checks.

Root Cause Analysis

The network service created and used P2P sockets toward a renderer-specified remote target port without consistently validating that port against Chrome’s port restrictions. P2PSocketManager::CreateSocket() and P2PSocketTcp’s Init() performed no port check at all, so a renderer could request a P2P socket whose remote endpoint targeted a restricted service port; the UDP send path checked only net::IsPortAllowedForIpEndpoint(packet.to) and never the scheme-specific restriction. The violated invariant is that renderer-controlled remote endpoints must be subject to the same port blocking used elsewhere, so WebRTC cannot be used to reach otherwise-blocked ports.

The fix, gated behind kEnforceP2PSocketPortRestrictions, adds an is_restricted_port check combining !net::IsPortAllowedForIpEndpoint(...) and !net::IsPortAllowedForScheme(port, "stun") at socket creation (CreateSocket, which simply returns without creating the socket), at TCP Init() (calls OnError() and returns), and on the UDP send path (OnError() and returns false). A deliberate exception skips validation only for the UDP creation case where the renderer initially passes port 0 (should_skip_port_validation). This tightens input validation on the trusted network-service side.

Key insight
The single mistake was trusting the renderer-supplied remote target port for P2P sockets and validating it inconsistently (partially on UDP send, not at all on creation/TCP init); the fix uniformly rejects restricted ports via IsPortAllowedForIpEndpoint() and IsPortAllowedForScheme(port, "stun") across creation, init, and send.

Attack Path

  1. Reach the P2P socket API A page (or compromised renderer) uses WebRTC to request P2P sockets from the network service via P2PSocketManager.
  2. Specify a restricted remote port The renderer supplies a remote target endpoint on a blocked port, e.g. 192.168.1.10:25 (SMTP), as in the regression tests.
  3. Bypass missing validation (pre-fix) CreateSocket()/P2PSocketTcp::Init() create the socket without any port check, and the UDP send path only checked IsPortAllowedForIpEndpoint, so the restricted target is accepted.
  4. Send crafted traffic to the blocked port The attacker drives the socket to send (STUN-formatted) datagrams/connections to the restricted port on an arbitrary reachable host, using WebRTC as a vehicle to reach services on ports the browser normally blocks (cross-protocol interaction / internal port reach is (inferred) as to concrete downstream effect).
  5. Post-fix rejection With kEnforceP2PSocketPortRestrictions enabled, creation returns early or the socket is destroyed via OnError(), and the send returns false, blocking the restricted target.

Impact Assessment

A renderer can direct network-service P2P sockets at restricted ports on arbitrary IP endpoints, letting WebRTC send attacker-influenced traffic to services on ports Chrome otherwise blocks — improper input validation exercised from the renderer against the network-service process. The precondition is access to the WebRTC P2P socket path (available to web content, more powerful from a compromised renderer). The advisory classifies this as improper input validation in WebRTC; no in-the-wild exploitation is stated, and any concrete cross-protocol or internal-service compromise beyond reaching the port is (inferred).

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
if
services/network/p2p/socket_tcp.cc
modified
if
services/network/p2p/socket_udp.cc
modified
TEST_F
services/network/p2p/socket_manager_unittest.cc
modified
TEST_F
services/network/p2p/socket_tcp_unittest.cc
modified
TEST_F
services/network/p2p/socket_udp_unittest.cc
modified

Files Changed

  • services/network/p2p/socket_manager.cc
  • services/network/p2p/socket.h
  • services/network/p2p/socket_manager_unittest.cc
  • services/network/p2p/socket_tcp_unittest.cc
  • services/network/p2p/socket_udp.cc
  • services/network/p2p/socket.cc
  • services/network/p2p/socket_tcp.cc
  • services/network/p2p/socket_udp_unittest.cc

Audit Directions

  • Renderer-supplied endpoints in the network service
    Audit other network-service entry points that accept renderer-provided IP/port targets to confirm they apply IsPortAllowedForIpEndpoint/IsPortAllowedForScheme before connecting or sending.
  • Inconsistent validation across a socket lifecycle
    Look for validation applied on one path (e.g. send) but not on creation/init, where an attacker chooses the unguarded entry point.
  • Scheme-aware port restrictions
    Review WebRTC/STUN/TURN and other protocol handlers for missing scheme-specific port checks (IsPortAllowedForScheme) that could let them reach blocked service ports.
From c4ef6a270b016c86dd63549090833fff6f2f9ffc Mon Sep 17 00:00:00 2001
From: Guido Urdaneta <[email protected]>
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 <[email protected]>
Commit-Queue: Guido Urdaneta <[email protected]>
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_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;
+    }
+  }
+
   net::HostPortPair dest_host_port_pair;
   // If there is a domain name, let's try it first, it's required by some proxy
   // to only take hostname for CONNECT. If it has been DNS resolved, the result
diff --git a/services/network/p2p/socket_udp.cc b/services/network/p2p/socket_udp.cc
index 790ef41a..90ff38b 100644
--- a/services/network/p2p/socket_udp.cc
+++ b/services/network/p2p/socket_udp.cc
@@ -6,6 +6,7 @@
 
 #include <tuple>
 
+#include "base/feature_list.h"
 #include "base/functional/bind.h"
 #include "base/memory/ptr_util.h"
 #include "base/metrics/histogram_functions.h"
@@ -417,9 +418,14 @@
   int64_t send_time_us = webrtc::TimeMicros();
   int64_t send_time_ms = send_time_us / 1000;
 
-  if (!net::IsPortAllowedForIpEndpoint(packet.to)) {
-    OnError();
-    return false;
+  if (base::FeatureList::IsEnabled(kEnforceP2PSocketPortRestrictions)) {
+    bool is_restricted_port =
+        !net::IsPortAllowedForIpEndpoint(packet.to) ||
+        !net::IsPortAllowedForScheme(packet.to.port(), "stun");
+    if (is_restricted_port) {
+      OnError();
+      return false;
+    }
   }
 
   // The peer is considered not connected until the first incoming STUN
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…

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 [email protected]

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