CVE-2026-79050
Overview
Background
- `kRestrictAbusePortsOnLocalhost`
- a
netfeature that blocks outgoing TCP and UDP connections to a configured list of local-host ports (thelocalhost_restrict_portsparameter). A blocked connect fails withERR_UNSAFE_PORTand is recorded in theNet.RestrictedLocalhostPortshistogram. - `IsPortAllowedForIpEndpoint`
- the function in
net/base/port_util.ccthat decides, for a resolvedIPEndPoint, whether that port restriction applies. The tests showTCPClientSocket::ConnectandUDPClientSocket::ConnectreturningERR_UNSAFE_PORTbased on its result. - Unspecified address (`0.0.0.0` / `::`)
- the all-zeros IPv4/IPv6 address. As the commit message says, POSIX-like operating systems route connections to it to the local host, so it acts as an alias for localhost.
- IPv4-mapped IPv6
- the
::ffff:a.b.c.dform, which carries an IPv4 address inside an IPv6 address. The fix normalizes it withConvertIPv4MappedIPv6ToIPv4before classifying.
Root Cause Analysis
Before the patch, IsPortAllowedForIpEndpoint returned true (allowed) for any endpoint whose address was not IsLoopback(). The restricted-port list was therefore only consulted for 127.0.0.0/8 and ::1. Connections to 0.0.0.0, to ::, and (per the new test vectors) to the IPv4-mapped forms of those addresses skipped the restriction. On POSIX-like systems those connections still reach services listening on the local host. So the feature’s rule, “abuse ports on localhost are unreachable”, could be bypassed just by using a different spelling of the local address.
The fix adds a helper, MayConnectToLocalhost, which first unmaps IPv4-mapped IPv6 addresses and then treats both IsLoopback() and IsZero() addresses as local. IsPortAllowedForIpEndpoint now falls through to the restricted-port check for all of these. This tightens the check: more addresses become subject to the block list. The in-code comment notes this is harmless on platforms where zero-address connections fail anyway.
0.0.0.0/::, also in IPv4-mapped form) reaches the same local services, so the restriction could be bypassed. The fix classifies these addresses as local too, via MayConnectToLocalhost.Attack Path
- Restriction is active
the user’s browser has
kRestrictAbusePortsOnLocalhostenabled with a non-emptylocalhost_restrict_portslist. TheNoParamSettest shows nothing is blocked without that parameter. - Attacker targets a zero address
instead of
127.0.0.1or[::1], the attacker makes the browser connect to0.0.0.0,[::], or an IPv4-mapped equivalent on a restricted port. Doing this from ordinary web content (for example a page-initiated request) is plausible (inferred), but other defenses such as private/local network access checks may apply independently and are not shown in this patch. - Port check is skipped
IsPortAllowedForIpEndpointsees a non-loopback address and returnstrue, soTCPClientSocket/UDPClientSocketconnect instead of failing withERR_UNSAFE_PORT. - Local service is reached on POSIX-like systems the OS routes the connection to the local host, so a service the feature was meant to shield receives attacker-influenced traffic. What that allows depends on the service (inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
fornet/base/port_util_unittest.cc |
modified | |
TESTnet/base/port_util_unittest.cc |
modified | |
fornet/socket/tcp_client_socket_unittest.cc |
modified | |
TestSocketPerformanceWatchernet/socket/tcp_client_socket_unittest.cc |
modified | |
fornet/socket/udp_socket_unittest.cc |
modified | |
TEST_Fnet/socket/udp_socket_unittest.cc |
modified |
Files Changed
net/socket/tcp_client_socket_unittest.ccnet/base/port_util.ccnet/socket/udp_socket_unittest.ccnet/base/port_util_unittest.cc
Audit Directions
- Localhost classification by address rangelook for other checks (private/local network access,
IsLocalhost-style helpers, socket APIs for apps/extensions, DevTools remote-debugging gates) that rely onIsLoopback()alone and miss0.0.0.0,::, or IPv4-mapped/IPv4-compatible forms. - Normalize before classifyingcheck that address classifiers handle IPv4-mapped IPv6 (
::ffff:127.0.0.1,::ffff:0.0.0.0) and are applied to the address that is actually connected to (after DNS resolution), not to the hostname or URL. - Allow-by-default early returnsreview security checks that start with “if not in category X, allow”. An incomplete definition of X silently turns into a bypass, so prefer classifiers that err toward including ambiguous addresses.
Patch
From 085f44318aea6217b5130ad12e8332e1be10599e Mon Sep 17 00:00:00 2001 From: Adam Rice <[email protected]> Date: Wed, 08 Jul 2026 08:38:42 -0700 Subject: [PATCH] Restrict abuse ports on 0.0.0.0 and :: as they are localhost aliases. On POSIX-like OSs, 0.0.0.0 and :: can be used as aliases for localhost, but the implementation of the kRestrictAbusePortsOnLocalhost feature didn't check for that. This CL modifies IsPortAllowedForIpEndpoint to also restrict ports on zero addresses. It also updates tests in net/socket/udp_socket_unittest.cc, net/socket/tcp_client_socket_unittest.cc, and net/base/port_util_unittest.cc to verify that the ports are blocked for all of 127.0.0.0/8, ::1, 0.0.0.0, and ::. Implemented by Gemini. Bug: 517364411 Change-Id: I1064c920a7cb98c262c77ae9b03febafa5193829 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7967099 Reviewed-by: Yoichi Osato <[email protected]> Commit-Queue: Adam Rice <[email protected]> Cr-Commit-Position: refs/heads/main@{#1658792} --- diff --git a/net/base/port_util.cc b/net/base/port_util.cc index 49d0f744..41cbfcaa 100644 --- a/net/base/port_util.cc +++ b/net/base/port_util.cc @@ -19,6 +19,7 @@ #include "base/strings/string_split.h" #include "base/strings/string_util.h" #include "net/base/features.h" +#include "net/base/ip_address.h" #include "net/base/ip_endpoint.h" #include "net/base/parse_number.h" #include "url/url_constants.h" @@ -154,6 +155,17 @@ constinit bool g_need_to_reset_restrict_localhost_ports = false; +// Returns true if connecting to `address` may reach a service on the local +// host. In addition to the loopback ranges this includes the unspecified +// address, which many operating systems route to loopback, and IPv4-mapped +// IPv6 forms of those addresses. +bool MayConnectToLocalhost(const IPAddress& address) { + const IPAddress unmapped = address.IsIPv4MappedIPv6() + ? ConvertIPv4MappedIPv6ToIPv4(address) + : address; + return unmapped.IsLoopback() || unmapped.IsZero(); +} + } // namespace bool IsPortValid(int port) { @@ -206,7 +218,10 @@ } // This function currently restricts only on localhost. - if (!endpoint.address().IsLoopback()) { + // 0.0.0.0 and :: are treated as localhost aliases on some platforms. + // On other platforms, connections to those addresses fail, so applying + // the same restriction is harmless. + if (!MayConnectToLocalhost(endpoint.address())) { return true; } diff --git a/net/base/port_util_unittest.cc b/net/base/port_util_unittest.cc index 0523bd9..2a04b071 100644 --- a/net/base/port_util_unittest.cc +++ b/net/base/port_util_unittest.cc @@ -57,22 +57,40 @@ ReloadLocalhostRestrictedPortsForTesting(); IPAddress public_address(8, 8, 8, 8); EXPECT_TRUE(IsPortAllowedForIpEndpoint(IPEndPoint(public_address, 12345))); + EXPECT_TRUE(IsPortAllowedForIpEndpoint( + IPEndPoint(ConvertIPv4ToIPv4MappedIPv6(public_address), 12345))); EXPECT_TRUE(IsPortAllowedForIpEndpoint(IPEndPoint(public_address, 443))); EXPECT_TRUE( IsPortAllowedForIpEndpoint(IPEndPoint(IPAddress::IPv4Localhost(), 443))); EXPECT_TRUE( IsPortAllowedForIpEndpoint(IPEndPoint(IPAddress::IPv6Localhost(), 443))); histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", 0); + + const IPAddress localhost_addresses[] = { + IPAddress::IPv4Localhost(), + IPAddress(127, 0, 0, 2), + IPAddress::IPv4AllZeros(), + IPAddress::IPv6Localhost(), + IPAddress::IPv6AllZeros(), + ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4Localhost()), + ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4AllZeros()), + }; + + int expected_count = 0; for (int port : {12345, 23456, 34567}) { - EXPECT_FALSE(IsPortAllowedForIpEndpoint( - IPEndPoint(IPAddress::IPv4Localhost(), port))); - EXPECT_FALSE(IsPortAllowedForIpEndpoint( - IPEndPoint(IPAddress::IPv6Localhost(), port))); + for (const auto& address : localhost_addresses) { + EXPECT_FALSE(IsPortAllowedForIpEndpoint(IPEndPoint(address, port))); + expected_count++; + } } - histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", 6); - histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", 12345, 2); - histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", 23456, 2); - histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", 34567, 2); + histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", + expected_count); + histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", 12345, + std::size(localhost_addresses)); + histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", 23456, + std::size(localhost_addresses)); + histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", 34567, + std::size(localhost_addresses)); } TEST(NetUtilTest, RestrictedAbusePortsLocalhostTestNoParamSet) { @@ -82,17 +100,29 @@ ReloadLocalhostRestrictedPortsForTesting(); IPAddress public_address(8, 8, 8, 8); EXPECT_TRUE(IsPortAllowedForIpEndpoint(IPEndPoint(public_address, 12345))); + EXPECT_TRUE(IsPortAllowedForIpEndpoint( + IPEndPoint(ConvertIPv4ToIPv4MappedIPv6(public_address), 12345))); EXPECT_TRUE(IsPortAllowedForIpEndpoint(IPEndPoint(public_address, 443))); EXPECT_TRUE( IsPortAllowedForIpEndpoint(IPEndPoint(IPAddress::IPv4Localhost(), 443))); EXPECT_TRUE( IsPortAllowedForIpEndpoint(IPEndPoint(IPAddress::IPv6Localhost(), 443))); histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", 0); + + const IPAddress localhost_addresses[] = { + IPAddress::IPv4Localhost(), + IPAddress(127, 0, 0, 2), + IPAddress::IPv4AllZeros(), + IPAddress::IPv6Localhost(), + IPAddress::IPv6AllZeros(), + ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4Localhost()), + ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4AllZeros()), + }; + for (int port : {12345, 23456, 34567}) { - EXPECT_TRUE(IsPortAllowedForIpEndpoint( - IPEndPoint(IPAddress::IPv4Localhost(), port))); - EXPECT_TRUE(IsPortAllowedForIpEndpoint( - IPEndPoint(IPAddress::IPv6Localhost(), port))); + for (const auto& address : localhost_addresses) { + EXPECT_TRUE(IsPortAllowedForIpEndpoint(IPEndPoint(address, port))); + } } histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", 0); } diff --git a/net/socket/tcp_client_socket_unittest.cc b/net/socket/tcp_client_socket_unittest.cc index 7ce8d38..493a666c 100644 --- a/net/socket/tcp_client_socket_unittest.cc +++ b/net/socket/tcp_client_socket_unittest.cc @@ -339,16 +339,33 @@ {{"localhost_restrict_ports", base::NumberToString(server_address.port())}}); ReloadLocalhostRestrictedPortsForTesting(); - TCPClientSocket socket(AddressList(server_address), nullptr, nullptr, nullptr, - NetLogSource(), handles::kInvalidNetworkHandle); + const IPAddress addresses_to_test[] = { + IPAddress::IPv4Localhost(), + IPAddress(127, 0, 0, 2), + IPAddress::IPv4AllZeros(), + IPAddress::IPv6Localhost(), + IPAddress::IPv6AllZeros(), + ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4Localhost()), + ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4AllZeros()), + }; - TestCompletionCallback connect_callback; - int connect_result = socket.Connect(connect_callback.callback()); - EXPECT_THAT(connect_callback.GetResult(connect_result), - IsError(ERR_UNSAFE_PORT)); - histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", 1); + int expected_count = 0; + for (const auto& address : addresses_to_test) { + TCPClientSocket socket( + AddressList(IPEndPoint(address, server_address.port())), nullptr, + nullptr, nullptr, NetLogSource(), handles::kInvalidNetworkHandle); + + TestCompletionCallback connect_callback; + int connect_result = socket.Connect(connect_callback.callback()); + EXPECT_THAT(connect_callback.GetResult(connect_result), + IsError(ERR_UNSAFE_PORT)); + expected_count++; + } + + histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", + expected_count); histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", - server_address.port(), 1); + server_address.port(), expected_count); } class TestSocketPerformanceWatcher : public SocketPerformanceWatcher { diff --git a/net/socket/udp_socket_unittest.cc b/net/socket/udp_socket_unittest.cc index b4d177c2..be44ded 100644 --- a/net/socket/udp_socket_unittest.cc +++ b/net/socket/udp_socket_unittest.cc @@ -400,14 +400,30 @@ {{"localhost_restrict_ports", base::NumberToString(server_address.port())}}); ReloadLocalhostRestrictedPortsForTesting(); - // Setup the client. - auto client = std::make_unique<UDPClientSocket>( - DatagramSocket::DEFAULT_BIND, NetLog::Get(), NetLogSource(), - handles::kInvalidNetworkHandle); - EXPECT_THAT(client->Connect(server_address), IsError(ERR_UNSAFE_PORT)); - histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", 1); + const IPAddress addresses_to_test[] = { + IPAddress::IPv4Localhost(), + IPAddress(127, 0, 0, 2), + IPAddress::IPv4AllZeros(), + IPAddress::IPv6Localhost(), + IPAddress::IPv6AllZeros(), + ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4Localhost()), + ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4AllZeros()), + }; + + int expected_count = 0; + for (const auto& address : addresses_to_test) { + auto client = std::make_unique<UDPClientSocket>( + DatagramSocket::DEFAULT_BIND, NetLog::Get(), NetLogSource(), + handles::kInvalidNetworkHandle); + EXPECT_THAT(client->Connect(IPEndPoint(address, server_address.port())), + IsError(ERR_UNSAFE_PORT)); + expected_count++; + } + + histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", + expected_count); histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", - server_address.port(), 1); + server_address.port(), expected_count); } TEST_F(UDPSocketTest, ConnectUsingNetworkRestrictedPort) {
Regression Test / PoC
diff --git a/net/base/port_util_unittest.cc b/net/base/port_util_unittest.cc
index 0523bd9..2a04b071 100644
--- a/net/base/port_util_unittest.cc
+++ b/net/base/port_util_unittest.cc
@@ -57,22 +57,40 @@
ReloadLocalhostRestrictedPortsForTesting();
IPAddress public_address(8, 8, 8, 8);
EXPECT_TRUE(IsPortAllowedForIpEndpoint(IPEndPoint(public_address, 12345)));
+ EXPECT_TRUE(IsPortAllowedForIpEndpoint(
+ IPEndPoint(ConvertIPv4ToIPv4MappedIPv6(public_address), 12345)));
EXPECT_TRUE(IsPortAllowedForIpEndpoint(IPEndPoint(public_address, 443)));
EXPECT_TRUE(
IsPortAllowedForIpEndpoint(IPEndPoint(IPAddress::IPv4Localhost(), 443)));
EXPECT_TRUE(
IsPortAllowedForIpEndpoint(IPEndPoint(IPAddress::IPv6Localhost(), 443)));
histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", 0);
+
+ const IPAddress localhost_addresses[] = {
+ IPAddress::IPv4Localhost(),
+ IPAddress(127, 0, 0, 2),
+ IPAddress::IPv4AllZeros(),
+ IPAddress::IPv6Localhost(),
+ IPAddress::IPv6AllZeros(),
+ ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4Localhost()),
+ ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4AllZeros()),
+ };
+
+ int expected_count = 0;
for (int port : {12345, 23456, 34567}) {
- EXPECT_FALSE(IsPortAllowedForIpEndpoint(
- IPEndPoint(IPAddress::IPv4Localhost(), port)));
- EXPECT_FALSE(IsPortAllowedForIpEndpoint(
- IPEndPoint(IPAddress::IPv6Localhost(), port)));
+ for (const auto& address : localhost_addresses) {
+ EXPECT_FALSE(IsPortAllowedForIpEndpoint(IPEndPoint(address, port)));
+ expected_count++;
+ }
}
- histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", 6);
- histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", 12345, 2);
- histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", 23456, 2);
- histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", 34567, 2);
+ histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts",
+ expected_count);
+ histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", 12345,
+ std::size(localhost_addresses));
+ histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", 23456,
+ std::size(localhost_addresses));
+ histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts", 34567,
+ std::size(localhost_addresses));
}
TEST(NetUtilTest, RestrictedAbusePortsLocalhostTestNoParamSet) {
@@ -82,17 +100,29 @@
ReloadLocalhostRestrictedPortsForTesting();
IPAddress public_address(8, 8, 8, 8);
EXPECT_TRUE(IsPortAllowedForIpEndpoint(IPEndPoint(public_address, 12345)));
+ EXPECT_TRUE(IsPortAllowedForIpEndpoint(
+ IPEndPoint(ConvertIPv4ToIPv4MappedIPv6(public_address), 12345)));
EXPECT_TRUE(IsPortAllowedForIpEndpoint(IPEndPoint(public_address, 443)));
EXPECT_TRUE(
IsPortAllowedForIpEndpoint(IPEndPoint(IPAddress::IPv4Localhost(), 443)));
EXPECT_TRUE(
IsPortAllowedForIpEndpoint(IPEndPoint(IPAddress::IPv6Localhost(), 443)));
histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", 0);
+
+ const IPAddress localhost_addresses[] = {
+ IPAddress::IPv4Localhost(),
+ IPAddress(127, 0, 0, 2),
+ IPAddress::IPv4AllZeros(),
+ IPAddress::IPv6Localhost(),
+ IPAddress::IPv6AllZeros(),
+ ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4Localhost()),
+ ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4AllZeros()),
+ };
+
for (int port : {12345, 23456, 34567}) {
- EXPECT_TRUE(IsPortAllowedForIpEndpoint(
- IPEndPoint(IPAddress::IPv4Localhost(), port)));
- EXPECT_TRUE(IsPortAllowedForIpEndpoint(
- IPEndPoint(IPAddress::IPv6Localhost(), port)));
+ for (const auto& address : localhost_addresses) {
+ EXPECT_TRUE(IsPortAllowedForIpEndpoint(IPEndPoint(address, port)));
+ }
}
histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", 0);
}
diff --git a/net/socket/tcp_client_socket_unittest.cc b/net/socket/tcp_client_socket_unittest.cc
index 7ce8d38..493a666c 100644
--- a/net/socket/tcp_client_socket_unittest.cc
+++ b/net/socket/tcp_client_socket_unittest.cc
@@ -339,16 +339,33 @@
{{"localhost_restrict_ports",
base::NumberToString(server_address.port())}});
ReloadLocalhostRestrictedPortsForTesting();
- TCPClientSocket socket(AddressList(server_address), nullptr, nullptr, nullptr,
- NetLogSource(), handles::kInvalidNetworkHandle);
+ const IPAddress addresses_to_test[] = {
+ IPAddress::IPv4Localhost(),
+ IPAddress(127, 0, 0, 2),
+ IPAddress::IPv4AllZeros(),
+ IPAddress::IPv6Localhost(),
+ IPAddress::IPv6AllZeros(),
+ ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4Localhost()),
+ ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4AllZeros()),
+ };
- TestCompletionCallback connect_callback;
- int connect_result = socket.Connect(connect_callback.callback());
- EXPECT_THAT(connect_callback.GetResult(connect_result),
- IsError(ERR_UNSAFE_PORT));
- histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", 1);
+ int expected_count = 0;
+ for (const auto& address : addresses_to_test) {
+ TCPClientSocket socket(
+ AddressList(IPEndPoint(address, server_address.port())), nullptr,
+ nullptr, nullptr, NetLogSource(), handles::kInvalidNetworkHandle);
+
+ TestCompletionCallback connect_callback;
+ int connect_result = socket.Connect(connect_callback.callback());
+ EXPECT_THAT(connect_callback.GetResult(connect_result),
+ IsError(ERR_UNSAFE_PORT));
+ expected_count++;
+ }
+
+ histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts",
+ expected_count);
histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts",
- server_address.port(), 1);
+ server_address.port(), expected_count);
}
class TestSocketPerformanceWatcher : public SocketPerformanceWatcher {
diff --git a/net/socket/udp_socket_unittest.cc b/net/socket/udp_socket_unittest.cc
index b4d177c2..be44ded 100644
--- a/net/socket/udp_socket_unittest.cc
+++ b/net/socket/udp_socket_unittest.cc
@@ -400,14 +400,30 @@
{{"localhost_restrict_ports",
base::NumberToString(server_address.port())}});
ReloadLocalhostRestrictedPortsForTesting();
- // Setup the client.
- auto client = std::make_unique<UDPClientSocket>(
- DatagramSocket::DEFAULT_BIND, NetLog::Get(), NetLogSource(),
- handles::kInvalidNetworkHandle);
- EXPECT_THAT(client->Connect(server_address), IsError(ERR_UNSAFE_PORT));
- histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts", 1);
+ const IPAddress addresses_to_test[] = {
+ IPAddress::IPv4Localhost(),
+ IPAddress(127, 0, 0, 2),
+ IPAddress::IPv4AllZeros(),
+ IPAddress::IPv6Localhost(),
+ IPAddress::IPv6AllZeros(),
+ ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4Localhost()),
+ ConvertIPv4ToIPv4MappedIPv6(IPAddress::IPv4AllZeros()),
+ };
+
+ int expected_count = 0;
+ for (const auto& address : addresses_to_test) {
+ auto client = std::make_unique<UDPClientSocket>(
+ DatagramSocket::DEFAULT_BIND, NetLog::Get(), NetLogSource(),
+ handles::kInvalidNetworkHandle);
+ EXPECT_THAT(client->Connect(IPEndPoint(address, server_address.port())),
+ IsError(ERR_UNSAFE_PORT));
+ expected_count++;
+ }
+
+ histogram_tester.ExpectTotalCount("Net.RestrictedLocalhostPorts",
+ expected_count);
histogram_tester.ExpectBucketCount("Net.RestrictedLocalhostPorts",
- server_address.port(), 1);
+ server_address.port(), expected_count);
}
TEST_F(UDPSocketTest, ConnectUsingNetworkRestrictedPort) {
Original Bug Report
Bypass of localhost port restrictions via unspecified addresses (0.0.0.0 and ::)
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The localhost port restriction mechanism designed to block connections to unsafe local services on POSIX platforms can be bypassed using unspecified IP addresses (0.0.0.0 and ::). Because Chromium’s port authorization checks rely on IPAddress::IsLoopback(), which does not recognize unspecified IP addresses, the blocklist is not applied. On POSIX operating systems, the kernel routes connections to unspecified addresses directly to the loopback interface, allowing access to restricted localhost ports.
Affected files:
net/base/port_util.ccnet/base/ip_address.cc
Estimated timestamp from git blame: 2025-05-09
Summary
The kRestrictAbusePortsOnLocalhost feature is a security mitigation designed to prevent network requests to restricted ports on localhost. However, this mitigation can potentially be bypassed on POSIX platforms (such as Linux, Android, and macOS) by using unspecified IP addresses like 0.0.0.0 or ::.
Root Cause Analysis
The bypass occurs in IsPortAllowedForIpEndpoint() (net/base/port_util.cc), which checks if the endpoint address is a loopback address:
bool IsPortAllowedForIpEndpoint(const IPEndPoint& endpoint) {
if (!base::FeatureList::IsEnabled(features::kRestrictAbusePortsOnLocalhost)) {
return true;
}
// This function currently restricts only on localhost.
if (!endpoint.address().IsLoopback()) {
return true;
}
...
However, IPAddress::IsLoopback() (net/base/ip_address.cc) only matches IPv4 loopback (127.0.0.0/8) and IPv6 loopback (::1):
bool IPAddress::IsLoopback() const {
// 127.0.0.1/8
if (IsIPv4())
return ip_address_[0] == 127;
// ::1
if (IsIPv6()) {
for (size_t i = 0; i + 1 < ip_address_.size(); ++i) {
if (ip_address_[i] != 0)
return false;
}
return ip_address_.back() == 1;
}
return false;
}
Because of this, 0.0.0.0 and :: are not recognized as loopback. On POSIX systems (Linux, Android, macOS), calling connect(2) or sendto(2) on a socket with 0.0.0.0 or :: as the destination address is rewritten by the kernel route lookup to loopback. Consequently, packets are delivered to services bound to loopback, but Chromium’s port restriction mechanism treats the endpoint as a non-loopback address and does not apply the port restrictions.
Potential Trigger Path
Note: These are potential steps to trigger the issue, as we have not verified them dynamically using functional exploit code.
- The
RestrictAbusePortsOnLocalhostfeature is active (e.g., on Android, restricting a port like30987). - A page served from a loopback origin (e.g.,
127.0.0.1or WebView content classified askLoopback) makes a request tohttp://0.0.0.0:30987/or targets0.0.0.0:30987via WebRTC (P2PSocketUdp::DoSend). - Local Network Access permits the request because
0.0.0.0is mapped toIPAddressSpace::kLoopback. TCPClientSocket::DoConnectorP2PSocketUdp::DoSendcallsIsPortAllowedForIpEndpoint()on{0.0.0.0, 30987}.- Since
0.0.0.0is not recognized as a loopback IP byIsLoopback(), the function returnstrue, allowing the connection. - The host kernel routes the connection to
127.0.0.1:30987, successfully reaching the restricted local service.
Suggested Fix
Ensure that IsPortAllowedForIpEndpoint() also treats unspecified/zero IP addresses (0.0.0.0 and ::) as restricted loopback equivalents. This can be done by checking endpoint.address().IsZero() in IsPortAllowedForIpEndpoint:
bool IsPortAllowedForIpEndpoint(const IPEndPoint& endpoint) {
if (!base::FeatureList::IsEnabled(features::kRestrictAbusePortsOnLocalhost)) {
return true;
}
// This function currently restricts only on localhost.
if (!endpoint.address().IsLoopback() && !endpoint.address().IsZero()) {
return true;
}
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.