Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in DirectSockets
DescriptionInsufficient policy enforcement in DirectSockets
ComponentDirectSockets
Bug ClassLogic Error
Tracker497081987
Fix commitd3e49ac66609 (chromium/src) +26/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Files Changed

  • chrome/browser/direct_sockets/direct_sockets_apitest.cc
  • content/browser/direct_sockets/direct_sockets_service_impl.cc
From d3e49ac66609a61ce148166ca6564c446d09477d Mon Sep 17 00:00:00 2001
From: Vlad Krot <[email protected]>
Date: Fri, 03 Apr 2026 04:56:14 -0700
Subject: [PATCH] [Direct Sockets] Add multicast check to UDP socket local network

There was a vulnerability reported, that UDP socket can be send to a
local network without permission if that address is multicast (which
should be considered local).

The browser test added verified that vulnerability existed.

Fixed: 497081987
Change-Id: Iacb0a64fb8bf550d1c18c87672aa6558093e821d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7726025
Commit-Queue: Vlad Krot <[email protected]>
Reviewed-by: Olga Korokhina <[email protected]>
Reviewed-by: Patryk Chodur <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1609752}
---

diff --git a/chrome/browser/direct_sockets/direct_sockets_apitest.cc b/chrome/browser/direct_sockets/direct_sockets_apitest.cc
index 7edbf17c..8392ab8 100644
--- a/chrome/browser/direct_sockets/direct_sockets_apitest.cc
+++ b/chrome/browser/direct_sockets/direct_sockets_apitest.cc
@@ -1753,4 +1753,27 @@
 
   EXPECT_EQ("InvalidAccessError", content::EvalJs(iwa_frame, script));
 }
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastSendWithoutPrivatePolicyBypass) {
+  content::RenderFrameHost* app_frame = InstallAndOpenIsolatedWebApp(
+      /*with_pna=*/false, /*with_multicast=*/false);
+
+  const std::string script = R"(
+    (async () => {
+      try {
+        const socket = new UDPSocket({ remoteAddress: '239.255.255.250', remotePort: 1900 });
+        const { writable } = await socket.opened;
+        const writer = writable.getWriter();
+        await writer.write({ data: new TextEncoder().encode("M-SEARCH * HTTP/1.1\r\n...") });
+        writer.releaseLock();
+        await socket.close();
+        return 'success';
+      } catch (e) {
+        return e.message;
+      }
+    })()
+  )";
+  EXPECT_EQ("Access to local network is blocked.", EvalJs(app_frame, script));
+}
 }  // namespace
diff --git a/content/browser/direct_sockets/direct_sockets_service_impl.cc b/content/browser/direct_sockets/direct_sockets_service_impl.cc
index e4ca4f07..151c5546 100644
--- a/content/browser/direct_sockets/direct_sockets_service_impl.cc
+++ b/content/browser/direct_sockets/direct_sockets_service_impl.cc
@@ -174,8 +174,10 @@
 bool RequiresPrivateNetworkAccess(const net::AddressList& addresses) {
   return std::ranges::any_of(
       addresses.endpoints(), [](const net::IPEndPoint& ip_endpoint) {
+        // All multicast endpoints require PNA.
         return network::IPAddressToIPAddressSpace(ip_endpoint.address()) ==
-               network::mojom::IPAddressSpace::kLocal;
+                   network::mojom::IPAddressSpace::kLocal ||
+               ip_endpoint.address().IsMulticast();
       });
 }
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/direct_sockets/direct_sockets_apitest.cc b/chrome/browser/direct_sockets/direct_sockets_apitest.cc
index 7edbf17c..8392ab8 100644
--- a/chrome/browser/direct_sockets/direct_sockets_apitest.cc
+++ b/chrome/browser/direct_sockets/direct_sockets_apitest.cc
@@ -1753,4 +1753,27 @@
 
   EXPECT_EQ("InvalidAccessError", content::EvalJs(iwa_frame, script));
 }
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastSendWithoutPrivatePolicyBypass) {
+  content::RenderFrameHost* app_frame = InstallAndOpenIsolatedWebApp(
+      /*with_pna=*/false, /*with_multicast=*/false);
+
+  const std::string script = R"(
+    (async () => {
+      try {
+        const socket = new UDPSocket({ remoteAddress: '239.255.255.250', remotePort: 1900 });
+        const { writable } = await socket.opened;
+        const writer = writable.getWriter();
+        await writer.write({ data: new TextEncoder().encode("M-SEARCH * HTTP/1.1\r\n...") });
+        writer.releaseLock();
+        await socket.close();
+        return 'success';
+      } catch (e) {
+        return e.message;
+      }
+    })()
+  )";
+  EXPECT_EQ("Access to local network is blocked.", EvalJs(app_frame, script));
+}
 }  // namespace
Loading diff…

Original Bug Report

reported by [email protected]

Potential: IWA Direct Sockets multicast spoofing via PNA and policy bypass

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: An Isolated Web App (IWA) can bypass permissions to send arbitrary multicast UDP packets to the local network. This occurs due to multicast IPs being misclassified as public, bypassing Private Network Access (PNA) checks, and missing multicast policy validation in RestrictedUDPSocket data-sending methods. This enables active local network spoofing (e.g., mDNS/SSDP) from an unauthorized context.

Affected files:

  • services/network/restricted_udp_socket.cc
  • content/browser/direct_sockets/direct_sockets_service_impl.cc
  • services/network/public/cpp/ip_address_space_util.cc

Estimated timestamp from git blame: 2025-09-01

Overview

There is a potential vulnerability in the Direct Sockets API where an Isolated Web App (IWA) can send multicast UDP traffic without possessing the direct-sockets-multicast permission or Private Network Access (PNA) permissions (e.g., LOCAL_NETWORK). This flaw arises from two chained logic errors:

  1. PNA Bypass: Multicast IP addresses are misclassified as public IP space.
  2. Multicast Policy Bypass: The RestrictedUDPSocket data-sending methods (Send and SendTo) fail to enforce the multicast permission flag, unlike the group membership methods (JoinGroup and LeaveGroup).

Technical Details

1. Private Network Access (PNA) Bypass When an IWA attempts to open a connected UDP socket, DirectSocketsServiceImpl::OnResolveCompleteForUDPSocket calls GetRequiredPermissions(resolved_addresses). This function checks if the IP address requires PNA permissions using network::IPAddressToIPAddressSpace().

In services/network/public/cpp/ip_address_space_util.cc, IPAddressToIPAddressSpace checks the IP against NonPublicAddressSpaceMap(). However, this map omits multicast ranges (IPv4 224.0.0.0/4 and IPv6 ff00::/8). Consequently, multicast addresses are classified as IPAddressSpace::kPublic. Because they are seen as public, GetRequiredPermissions returns an empty list, completely bypassing PNA checks and prompts.

2. Missing Multicast Check in RestrictedUDPSocket After bypassing PNA, the browser creates a RestrictedUDPSocket in the Network Service. While the browser correctly passes allow_multicast = false to the Network Service, services/network/restricted_udp_socket.cc only checks this flag in JoinGroup and LeaveGroup.

The Send method (used for CONNECTED sockets) and SendTo method (used for BOUND sockets) unconditionally forward data to the underlying OS socket. The OS happily routes the UDP datagram to the multicast group.

Suggested Attacker Steps

(Note: These are potential steps based on code analysis; our tooling agent does not currently have the ability to run code to produce a live PoC).

  1. An attacker creates a malicious IWA that declares the base direct-sockets permission in its manifest, but deliberately omits direct-sockets-multicast and direct-sockets-private.
  2. A victim installs and runs the IWA.
  3. The IWA’s JavaScript executes the following to target a local SSDP multicast group:
    const socket = new UDPSocket({ remoteAddress: '239.255.255.250', remotePort: 1900 });
    const { writable } = await socket.opened;
    
    (Because 239.255.255.250 is classified as public, the socket connects immediately without triggering a PNA local network prompt).
  4. The IWA writes a spoofed SSDP discovery payload to the stream:
    const writer = writable.getWriter();
    await writer.write({ data: new TextEncoder().encode("M-SEARCH * HTTP/1.1\r\n...") });
    
    (Because RestrictedUDPSocket::Send lacks the allow_multicast_ check, the packet is emitted onto the local network).

Suggested Fix

  1. Fix the IP Space Map: In services/network/public/cpp/ip_address_space_util.cc, update NonPublicAddressSpaceMap() to include the IPv4 (224.0.0.0/4) and IPv6 (ff00::/8) multicast blocks so they are treated as kLocal (or mapped to a new space that strictly requires PNA permissions).
  2. Enforce Multicast Checks:
    • In RestrictedUDPSocket::SendTo, check if dest_addr is a multicast address. If it is, verify that allow_multicast_ is true.
    • In DirectSocketsServiceImpl::OnResolveCompleteForUDPSocket, check if the resolved address is a multicast address. If it is, fail the connection immediately if IsMulticastAllowed(context_) is false, preventing a connected socket from being bound to a multicast destination without permissions.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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