CVE-2026-7962
Overview
Files Changed
chrome/browser/direct_sockets/direct_sockets_apitest.cccontent/browser/direct_sockets/direct_sockets_service_impl.cc
Patch
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(); }); }
Regression Test / PoC
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
Original Bug Report
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.cccontent/browser/direct_sockets/direct_sockets_service_impl.ccservices/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:
- PNA Bypass: Multicast IP addresses are misclassified as public IP space.
- Multicast Policy Bypass: The
RestrictedUDPSocketdata-sending methods (SendandSendTo) fail to enforce the multicast permission flag, unlike the group membership methods (JoinGroupandLeaveGroup).
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).
- An attacker creates a malicious IWA that declares the base
direct-socketspermission in its manifest, but deliberately omitsdirect-sockets-multicastanddirect-sockets-private. - A victim installs and runs the IWA.
- The IWA’s JavaScript executes the following to target a local SSDP multicast group:
(Because
const socket = new UDPSocket({ remoteAddress: '239.255.255.250', remotePort: 1900 }); const { writable } = await socket.opened;239.255.255.250is classified as public, the socket connects immediately without triggering a PNA local network prompt). - The IWA writes a spoofed SSDP discovery payload to the stream:
(Because
const writer = writable.getWriter(); await writer.write({ data: new TextEncoder().encode("M-SEARCH * HTTP/1.1\r\n...") });RestrictedUDPSocket::Sendlacks theallow_multicast_check, the packet is emitted onto the local network).
Suggested Fix
- Fix the IP Space Map: In
services/network/public/cpp/ip_address_space_util.cc, updateNonPublicAddressSpaceMap()to include the IPv4 (224.0.0.0/4) and IPv6 (ff00::/8) multicast blocks so they are treated askLocal(or mapped to a new space that strictly requires PNA permissions). - Enforce Multicast Checks:
- In
RestrictedUDPSocket::SendTo, check ifdest_addris a multicast address. If it is, verify thatallow_multicast_is true. - In
DirectSocketsServiceImpl::OnResolveCompleteForUDPSocket, check if the resolved address is a multicast address. If it is, fail the connection immediately ifIsMulticastAllowed(context_)is false, preventing a connected socket from being bound to a multicast destination without permissions.
- In
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.