Medium watch CVSS 4.3 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Network
DescriptionIncorrect authorization in Network
ComponentNetwork
Bug ClassAuthorization Error
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.
Tracker517364411
Fix commit085f44318aea (chromium/src) +106/-28
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-07-08 (public git)
Disclosed2026-08-25
Patch gap48 days — fix public in source before the advisory

Background

`kRestrictAbusePortsOnLocalhost`
a net feature that blocks outgoing TCP and UDP connections to a configured list of local-host ports (the localhost_restrict_ports parameter). A blocked connect fails with ERR_UNSAFE_PORT and is recorded in the Net.RestrictedLocalhostPorts histogram.
`IsPortAllowedForIpEndpoint`
the function in net/base/port_util.cc that decides, for a resolved IPEndPoint, whether that port restriction applies. The tests show TCPClientSocket::Connect and UDPClientSocket::Connect returning ERR_UNSAFE_PORT based 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.d form, which carries an IPv4 address inside an IPv6 address. The fix normalizes it with ConvertIPv4MappedIPv6ToIPv4 before 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.

Key insight
The localhost port block identified “localhost” by the address being in the loopback range. But the unspecified address (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

  1. Restriction is active the user’s browser has kRestrictAbusePortsOnLocalhost enabled with a non-empty localhost_restrict_ports list. The NoParamSet test shows nothing is blocked without that parameter.
  2. Attacker targets a zero address instead of 127.0.0.1 or [::1], the attacker makes the browser connect to 0.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.
  3. Port check is skipped IsPortAllowedForIpEndpoint sees a non-loopback address and returns true, so TCPClientSocket/UDPClientSocket connect instead of failing with ERR_UNSAFE_PORT.
  4. 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

The advisory describes “Incorrect authorization in Network” and gives no in-the-wild status. The patch shows the browser’s network stack could bypass a configured block on connecting to restricted local-host ports, by using the unspecified address on POSIX-like platforms. An attacker who can make the browser connect to a chosen address could reach local services the restriction was meant to protect. Any compromise of those services is outside what the patch shows (inferred). The bug only matters when the restriction feature is enabled with a port list.

Changed Functions

FunctionChangeNotes
for
net/base/port_util_unittest.cc
modified
TEST
net/base/port_util_unittest.cc
modified
for
net/socket/tcp_client_socket_unittest.cc
modified
TestSocketPerformanceWatcher
net/socket/tcp_client_socket_unittest.cc
modified
for
net/socket/udp_socket_unittest.cc
modified
TEST_F
net/socket/udp_socket_unittest.cc
modified

Files Changed

  • net/socket/tcp_client_socket_unittest.cc
  • net/base/port_util.cc
  • net/socket/udp_socket_unittest.cc
  • net/base/port_util_unittest.cc

Audit Directions

  • Localhost classification by address range
    look for other checks (private/local network access, IsLocalhost-style helpers, socket APIs for apps/extensions, DevTools remote-debugging gates) that rely on IsLoopback() alone and miss 0.0.0.0, ::, or IPv4-mapped/IPv4-compatible forms.
  • Normalize before classifying
    check 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 returns
    review 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.
From 085f44318aea6217b5130ad12e8332e1be10599e Mon Sep 17 00:00:00 2001
From: Adam Rice <ricea@chromium.org>
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 <yoichio@chromium.org>
Commit-Queue: Adam Rice <ricea@chromium.org>
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) {
Loading diff…

Regression Test / PoC

shipped with the fix
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) {
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • net/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.

  1. The RestrictAbusePortsOnLocalhost feature is active (e.g., on Android, restricting a port like 30987).
  2. A page served from a loopback origin (e.g., 127.0.0.1 or WebView content classified as kLoopback) makes a request to http://0.0.0.0:30987/ or targets 0.0.0.0:30987 via WebRTC (P2PSocketUdp::DoSend).
  3. Local Network Access permits the request because 0.0.0.0 is mapped to IPAddressSpace::kLoopback.
  4. TCPClientSocket::DoConnect or P2PSocketUdp::DoSend calls IsPortAllowedForIpEndpoint() on {0.0.0.0, 30987}.
  5. Since 0.0.0.0 is not recognized as a loopback IP by IsLoopback(), the function returns true, allowing the connection.
  6. 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.

View on issue tracker
Links in the report