CVE-2026-79255
Overview
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.
IsPortAllowedForIpEndpoint() and IsPortAllowedForScheme(port, "stun") across creation, init, and send.Attack Path
- Reach the P2P socket API
A page (or compromised renderer) uses WebRTC to request P2P sockets from the network service via
P2PSocketManager. - 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. - Bypass missing validation (pre-fix)
CreateSocket()/P2PSocketTcp::Init()create the socket without any port check, and the UDP send path only checkedIsPortAllowedForIpEndpoint, so the restricted target is accepted. - 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). - Post-fix rejection
With
kEnforceP2PSocketPortRestrictionsenabled, creation returns early or the socket is destroyed viaOnError(), and the send returnsfalse, blocking the restricted target.
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ProxyResolvingClientSocketFactoryservices/network/p2p/socket.h |
modified | |
P2PMessageThrottlerservices/network/p2p/socket.h |
modified | |
COMPONENT_EXPORTservices/network/p2p/socket.h |
modified | |
ifservices/network/p2p/socket_manager.cc |
modified | |
ifservices/network/p2p/socket_tcp.cc |
modified | |
ifservices/network/p2p/socket_udp.cc |
modified | |
TEST_Fservices/network/p2p/socket_manager_unittest.cc |
modified | |
TEST_Fservices/network/p2p/socket_tcp_unittest.cc |
modified | |
TEST_Fservices/network/p2p/socket_udp_unittest.cc |
modified |
Files Changed
services/network/p2p/socket_manager.ccservices/network/p2p/socket.hservices/network/p2p/socket_manager_unittest.ccservices/network/p2p/socket_tcp_unittest.ccservices/network/p2p/socket_udp.ccservices/network/p2p/socket.ccservices/network/p2p/socket_tcp.ccservices/network/p2p/socket_udp_unittest.cc
Audit Directions
- Renderer-supplied endpoints in the network serviceAudit other network-service entry points that accept renderer-provided IP/port targets to confirm they apply
IsPortAllowedForIpEndpoint/IsPortAllowedForSchemebefore connecting or sending. - Inconsistent validation across a socket lifecycleLook 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 restrictionsReview WebRTC/STUN/TURN and other protocol handlers for missing scheme-specific port checks (
IsPortAllowedForScheme) that could let them reach blocked service ports.
Patch
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_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
Regression Test / PoC
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
Original Bug Report
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.ccservices/network/p2p/socket_tcp.ccservices/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.
- From a compromised renderer, use
BrowserInterfaceBrokerto requestnetwork::mojom::P2PSocketManager. - Call
CreateSocketwithtype=P2P_SOCKET_STUN_TCP_CLIENTandremote_addresspointing to an internal SMTP server (e.g.,192.168.1.10:25). - Wait for the
SocketCreatedMojo callback, confirming the TCP connection. - 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. - 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.