Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Network
DescriptionMissing authorization in Network
ComponentNetwork
Bug ClassLogic Error
Tracker517608454
Fix commit405665065117 (chromium/src) +548/-25
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
chrome/browser/direct_sockets/direct_sockets_apitest.cc
modified

Files Changed

  • chrome/browser/about_flags.cc
  • chrome/browser/direct_sockets/direct_sockets_apitest.cc
  • chrome/browser/flag-metadata.json
From 405665065117459f6a72161b865c8244742b21c7 Mon Sep 17 00:00:00 2001
From: Tsuyoshi Horo <[email protected]>
Date: Mon, 13 Jul 2026 03:14:09 -0700
Subject: [PATCH] [direct sockets] Gate UDP multicast send/connect on permission policy

RestrictedUDPSocket gated JoinGroup() and LeaveGroup() on the per-socket
allow_multicast_ flag, but SendTo() and Connect() allowed multicast
destination addresses even if the 'direct-sockets-multicast' Permissions
Policy was absent.

This CL introduces the following changes:
- Adds feature flag
  kDirectSocketsUdpSendRequireMulticastPermissionPolicy (enabled by
  default) and registers a chrome://flags entry to allow bypassing the
  restriction.
- Defines a new net error code net::ERR_MULTICAST_NOT_ALLOWED (-190).
- Checks multicast permission policy in DirectSocketsServiceImpl when
  resolving hostnames for connected UDP sockets, returning
  net::ERR_MULTICAST_NOT_ALLOWED if missing.
- Checks multicast permission policy in RestrictedUDPSocket::SendTo and
  OnResolveCompleteForSendTo for bound UDP sockets.
- Emits informative console error messages in renderer UDPSocket and
  UDPWritableStreamWrapper when ERR_MULTICAST_NOT_ALLOWED occurs.
- Updates unit tests and browser tests to run parameterized test suites
  covering enabled and disabled feature states, IP literals, and
  hostname resolution.

Bug: 517608454
Change-Id: Id9fc8d4f421424beb77826bb79750462fbe4b3f1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8037701
Commit-Queue: Tsuyoshi Horo <[email protected]>
Reviewed-by: Vlad Krot <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1661023}
---

diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index b23651c6..72c457e3 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -5473,6 +5473,15 @@
      flag_descriptions::kDirectSocketsInSharedWorkersName,
      flag_descriptions::kDirectSocketsInSharedWorkersDescription, kOsDesktop,
      FEATURE_VALUE_TYPE(blink::features::kDirectSocketsInSharedWorkers)},
+    {"direct-sockets-udp-send-require-multicast-permission-policy",
+     flag_descriptions::
+         kDirectSocketsUdpSendRequireMulticastPermissionPolicyName,
+     flag_descriptions::
+         kDirectSocketsUdpSendRequireMulticastPermissionPolicyDescription,
+     kOsDesktop,
+     FEATURE_VALUE_TYPE(
+         network::features::
+             kDirectSocketsUdpSendRequireMulticastPermissionPolicy)},
 #if BUILDFLAG(IS_CHROMEOS)
     {"enable-chromeos-isolated-web-app-set-shape",
      flag_descriptions::kEnableChromeOSIsolatedWebAppSetShapeName,
diff --git a/chrome/browser/direct_sockets/direct_sockets_apitest.cc b/chrome/browser/direct_sockets/direct_sockets_apitest.cc
index 4ecfa8f..095fff9c7 100644
--- a/chrome/browser/direct_sockets/direct_sockets_apitest.cc
+++ b/chrome/browser/direct_sockets/direct_sockets_apitest.cc
@@ -773,6 +773,56 @@
     web_app::IsolatedWebAppUrlInfo url_info = app->Install(profile()).value();
     return OpenApp(url_info.app_id());
   }
+
+  void TestMulticastSend(bool with_pna,
+                         bool with_multicast,
+                         bool connected_else_bound,
+                         std::string_view expected_result) {
+    content::RenderFrameHost* app_frame =
+        InstallAndOpenIsolatedWebApp(with_pna, with_multicast);
+
+    std::string script;
+    if (connected_else_bound) {
+      script = R"(
+        (async () => {
+          try {
+            const socket = new UDPSocket({
+              remoteAddress: '239.255.255.250',
+              remotePort: 1900
+            });
+            await socket.opened;
+            await socket.close();
+            return 'success';
+          } catch (e) {
+            return e.message;
+          }
+        })()
+      )";
+    } else {
+      script = R"(
+        (async () => {
+          try {
+            const socket = new UDPSocket({
+              localAddress: '0.0.0.0'
+            });
+            const { writable } = await socket.opened;
+            const writer = writable.getWriter();
+            await writer.write({
+              data: new Uint8Array([1, 2, 3]),
+              remoteAddress: '239.255.255.250',
+              remotePort: 1900
+            });
+            writer.releaseLock();
+            await socket.close();
+            return 'success';
+          } catch (e) {
+            return e.message;
+          }
+        })()
+      )";
+    }
+    EXPECT_EQ(expected_result, EvalJs(app_frame, script));
+  }
 };
 
 using IsolatedWebAppMulticastApiTest = IsolatedWebAppApiTest;
@@ -1835,25 +1885,55 @@
 }
 
 IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
-                       MulticastSendWithoutPrivatePolicyBypass) {
-  content::RenderFrameHost* app_frame = InstallAndOpenIsolatedWebApp(
-      /*with_pna=*/false, /*with_multicast=*/false);
+                       MulticastConnectedWithPnaAndMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/true, /*with_multicast=*/true,
+                    /*connected_else_bound=*/true, "success");
+}
 
-  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));
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastBoundWithPnaAndMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/true, /*with_multicast=*/true,
+                    /*connected_else_bound=*/false, "success");
+}
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastConnectedWithPnaWithoutMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/true, /*with_multicast=*/false,
+                    /*connected_else_bound=*/true, "Network Error.");
+}
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastBoundWithPnaWithoutMulticastPolicy) {
+  TestMulticastSend(
+      /*with_pna=*/true, /*with_multicast=*/false,
+      /*connected_else_bound=*/false,
+      "Stream aborted by the remote: net::ERR_MULTICAST_NOT_ALLOWED");
+}
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastConnectedWithoutPnaWithMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/false, /*with_multicast=*/true,
+                    /*connected_else_bound=*/true,
+                    "Access to local network is blocked.");
+}
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastBoundWithoutPnaWithMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/false, /*with_multicast=*/true,
+                    /*connected_else_bound=*/false,
+                    "Access to local network is blocked.");
+}
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastConnectedWithoutPnaWithoutMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/false, /*with_multicast=*/false,
+                    /*connected_else_bound=*/true, "Network Error.");
+}
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastBoundWithoutPnaWithoutMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/false, /*with_multicast=*/false,
+                    /*connected_else_bound=*/false,
+                    "Access to local network is blocked.");
 }
 }  // namespace
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 7f42d3b0..f571ee3b 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -2555,6 +2555,11 @@
     "expiry_milestone": 170
   },
   {
+    "name": "direct-sockets-udp-send-require-multicast-permission-policy",
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 4ecfa8f..095fff9c7 100644
--- a/chrome/browser/direct_sockets/direct_sockets_apitest.cc
+++ b/chrome/browser/direct_sockets/direct_sockets_apitest.cc
@@ -773,6 +773,56 @@
     web_app::IsolatedWebAppUrlInfo url_info = app->Install(profile()).value();
     return OpenApp(url_info.app_id());
   }
+
+  void TestMulticastSend(bool with_pna,
+                         bool with_multicast,
+                         bool connected_else_bound,
+                         std::string_view expected_result) {
+    content::RenderFrameHost* app_frame =
+        InstallAndOpenIsolatedWebApp(with_pna, with_multicast);
+
+    std::string script;
+    if (connected_else_bound) {
+      script = R"(
+        (async () => {
+          try {
+            const socket = new UDPSocket({
+              remoteAddress: '239.255.255.250',
+              remotePort: 1900
+            });
+            await socket.opened;
+            await socket.close();
+            return 'success';
+          } catch (e) {
+            return e.message;
+          }
+        })()
+      )";
+    } else {
+      script = R"(
+        (async () => {
+          try {
+            const socket = new UDPSocket({
+              localAddress: '0.0.0.0'
+            });
+            const { writable } = await socket.opened;
+            const writer = writable.getWriter();
+            await writer.write({
+              data: new Uint8Array([1, 2, 3]),
+              remoteAddress: '239.255.255.250',
+              remotePort: 1900
+            });
+            writer.releaseLock();
+            await socket.close();
+            return 'success';
+          } catch (e) {
+            return e.message;
+          }
+        })()
+      )";
+    }
+    EXPECT_EQ(expected_result, EvalJs(app_frame, script));
+  }
 };
 
 using IsolatedWebAppMulticastApiTest = IsolatedWebAppApiTest;
@@ -1835,25 +1885,55 @@
 }
 
 IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
-                       MulticastSendWithoutPrivatePolicyBypass) {
-  content::RenderFrameHost* app_frame = InstallAndOpenIsolatedWebApp(
-      /*with_pna=*/false, /*with_multicast=*/false);
+                       MulticastConnectedWithPnaAndMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/true, /*with_multicast=*/true,
+                    /*connected_else_bound=*/true, "success");
+}
 
-  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));
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastBoundWithPnaAndMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/true, /*with_multicast=*/true,
+                    /*connected_else_bound=*/false, "success");
+}
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastConnectedWithPnaWithoutMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/true, /*with_multicast=*/false,
+                    /*connected_else_bound=*/true, "Network Error.");
+}
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastBoundWithPnaWithoutMulticastPolicy) {
+  TestMulticastSend(
+      /*with_pna=*/true, /*with_multicast=*/false,
+      /*connected_else_bound=*/false,
+      "Stream aborted by the remote: net::ERR_MULTICAST_NOT_ALLOWED");
+}
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastConnectedWithoutPnaWithMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/false, /*with_multicast=*/true,
+                    /*connected_else_bound=*/true,
+                    "Access to local network is blocked.");
+}
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastBoundWithoutPnaWithMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/false, /*with_multicast=*/true,
+                    /*connected_else_bound=*/false,
+                    "Access to local network is blocked.");
+}
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastConnectedWithoutPnaWithoutMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/false, /*with_multicast=*/false,
+                    /*connected_else_bound=*/true, "Network Error.");
+}
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppApiTest,
+                       MulticastBoundWithoutPnaWithoutMulticastPolicy) {
+  TestMulticastSend(/*with_pna=*/false, /*with_multicast=*/false,
+                    /*connected_else_bound=*/false,
+                    "Access to local network is blocked.");
 }
 }  // namespace
diff --git a/content/browser/direct_sockets/direct_sockets_udp_browsertest.cc b/content/browser/direct_sockets/direct_sockets_udp_browsertest.cc
index ac1b529..0b07f45 100644
--- a/content/browser/direct_sockets/direct_sockets_udp_browsertest.cc
+++ b/content/browser/direct_sockets/direct_sockets_udp_browsertest.cc
@@ -24,7 +24,9 @@
 #include "net/base/ip_address.h"
 #include "net/base/ip_endpoint.h"
 #include "net/dns/host_resolver.h"
+#include "net/dns/mock_host_resolver.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
+#include "services/network/public/cpp/features.h"
 #include "services/network/public/mojom/network_context.mojom.h"
 #include "services/network/public/mojom/udp_socket.mojom.h"
 #include "services/network/test/test_network_context.h"
@@ -135,15 +137,16 @@
     return server_socket_;
   }
 
+ protected:
+  std::unique_ptr<test::IsolatedWebAppContentBrowserClient> client_;
+  std::unique_ptr<content::test::AsyncJsRunner> runner_;
+
  private:
   BrowserContext* browser_context() {
     return shell()->web_contents()->GetBrowserContext();
   }
 
   mojo::Remote<network::mojom::UDPSocket> server_socket_;
-
-  std::unique_ptr<test::IsolatedWebAppContentBrowserClient> client_;
-  std::unique_ptr<content::test::AsyncJsRunner> runner_;
 };
 
 IN_PROC_BROWSER_TEST_F(DirectSocketsUdpBrowserTest, CloseUdp) {
@@ -619,4 +622,170 @@
       ::testing::StartsWith("closeUdp failed"));
 }
 
+struct DirectSocketsMulticastBrowserTestParams {
+  bool flag_enabled;
+  bool has_permission_policy;
+  bool connected_else_bound_socket;
+  bool use_hostname;
+};
+
+class DirectSocketsMulticastBrowserTest
+    : public DirectSocketsUdpBrowserTest,
+      public testing::WithParamInterface<
+          DirectSocketsMulticastBrowserTestParams> {
+ public:
+#if BUILDFLAG(IS_CHROMEOS)
+  DirectSocketsMulticastBrowserTest() {
+    chromeos::PermissionBrokerClient::InitializeFake();
+    FirewallHoleDelegate::SetAlwaysOpenFirewallHoleForTesting(true);
+  }
+
+  ~DirectSocketsMulticastBrowserTest() override {
+    chromeos::PermissionBrokerClient::Shutdown();
+    FirewallHoleDelegate::SetAlwaysOpenFirewallHoleForTesting(false);
+  }
+#endif  // BUILDFLAG(IS_CHROMEOS)
+
+  void SetUpInProcessBrowserTestFixture() override {
+    DirectSocketsUdpBrowserTest::SetUpInProcessBrowserTestFixture();
+    if (GetParam().flag_enabled) {
+      feature_list_.InitAndEnableFeature(
+          network::features::
+              kDirectSocketsUdpSendRequireMulticastPermissionPolicy);
+    } else {
+      feature_list_.InitAndDisableFeature(
+          network::features::
+              kDirectSocketsUdpSendRequireMulticastPermissionPolicy);
+    }
+  }
+
+ protected:
+  void SetUpOnMainThread() override {
+    ContentBrowserTest::SetUpOnMainThread();
+
+    // The mock DNS rule must be added before `NavigateToURL` is called.
+    // Calling `NavigateToURL` initiates host resolution, which locks the
+    // resolver and prevents subsequent modifications (triggering a check
+    // failure).
+    host_resolver()->AddRule("mcast.test", "224.0.0.251");
+
+    client_ = CreateContentBrowserClient();
+    runner_ =
+        std::make_unique<content::test::AsyncJsRunner>(shell()->web_contents());
+
+    ASSERT_TRUE(NavigateToURL(shell(), GetTestPageURL()));
+  }
+
+  std::unique_ptr<test::IsolatedWebAppContentBrowserClient>
+  CreateContentBrowserClient() override {
+    return std::make_unique<test::IsolatedWebAppContentBrowserClient>(
+        url::Origin::Create(GetTestPageURL()));
+  }
+
+  GURL GetTestPageURL() override {
+    return test::FileWithHeaders("/direct_sockets/udp.html")
+        .WithCOIHeaders()
+        .WithPermissionsPolicy("cross-origin-isolated", "(self)")
+        .WithPermissionsPolicy("direct-sockets", "(self)")
+        .WithPermissionsPolicy("local-network", "(self)")
+        .WithPermissionsPolicy("loopback-network", "(self)")
+        .WithPermissionsPolicy(
+            "direct-sockets-multicast",
+            GetParam().has_permission_policy ? "(self)" : "()")
+        .Build(embedded_test_server());
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    DirectSocketsMulticastBrowserTest,
+    testing::Values(
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/false, /*has_permission_policy=*/false,
+            /*connected_else_bound_socket=*/false, /*use_hostname=*/false},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/false, /*has_permission_policy=*/false,
+            /*connected_else_bound_socket=*/false, /*use_hostname=*/true},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/false, /*has_permission_policy=*/false,
+            /*connected_else_bound_socket=*/true, /*use_hostname=*/false},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/false, /*has_permission_policy=*/false,
+            /*connected_else_bound_socket=*/true, /*use_hostname=*/true},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/false, /*has_permission_policy=*/true,
+            /*connected_else_bound_socket=*/false, /*use_hostname=*/false},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/false, /*has_permission_policy=*/true,
+            /*connected_else_bound_socket=*/false, /*use_hostname=*/true},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/false, /*has_permission_policy=*/true,
+            /*connected_else_bound_socket=*/true, /*use_hostname=*/false},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/false, /*has_permission_policy=*/true,
+            /*connected_else_bound_socket=*/true, /*use_hostname=*/true},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/true, /*has_permission_policy=*/false,
+            /*connected_else_bound_socket=*/false, /*use_hostname=*/false},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/true, /*has_permission_policy=*/false,
+            /*connected_else_bound_socket=*/false, /*use_hostname=*/true},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/true, /*has_permission_policy=*/false,
+            /*connected_else_bound_socket=*/true, /*use_hostname=*/false},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/true, /*has_permission_policy=*/false,
+            /*connected_else_bound_socket=*/true, /*use_hostname=*/true},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/true, /*has_permission_policy=*/true,
+            /*connected_else_bound_socket=*/false, /*use_hostname=*/false},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/true, /*has_permission_policy=*/true,
+            /*connected_else_bound_socket=*/false, /*use_hostname=*/true},
+        DirectSocketsMulticastBrowserTestParams{
+            /*flag_enabled=*/true, /*has_permission_policy=*/true,
+            /*connected_else_bound_socket=*/true, /*use_hostname=*/false},
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Bypass of multicast permission policy in RestrictedUDPSocket::SendTo

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 RestrictedUDPSocket class fails to check the allow_multicast_ permission flag during outgoing SendTo operations. This allows a document or context (such as an Isolated Web App) lacking the direct-sockets-multicast Permissions Policy to transmit arbitrary UDP datagrams to local network multicast groups. This bypass is possible using both direct IP literals and hostnames resolved via DNS.

Affected files:

  • services/network/restricted_udp_socket.cc
  • services/network/restricted_udp_socket.h

Estimated timestamp from git blame: 2025-09-15

Root Cause Analysis

In services/network/restricted_udp_socket.cc, the RestrictedUDPSocket class handles Direct Sockets UDP capabilities on a per-socket basis, tracking whether multicast operations are permitted via the allow_multicast_ member. While this flag is checked during JoinGroup and LeaveGroup Mojo calls, it is ignored on outgoing SendTo operations.

Specifically, in the raw IP literal path of RestrictedUDPSocket::SendTo:

  if (net::IPAddress address; address.AssignFromIPLiteral(dest_addr.host())) {
    udp_socket_->SendTo(net::IPEndPoint(std::move(address), dest_addr.port()),
                        data, traffic_annotation_, std::move(callback));
    return;
  }

And in the DNS resolution path within RestrictedUDPSocket::OnResolveCompleteForSendTo:

void RestrictedUDPSocket::OnResolveCompleteForSendTo(
    std::vector<uint8_t> data,
    SendToCallback callback,
    int result,
    const net::ResolveErrorInfo&,
    const net::AddressList& resolved_addresses,
    const net::HostResolverEndpointResults&) {
  if (result != net::OK) {
    std::move(callback).Run(result);
    return;
  }

  udp_socket_->SendTo(resolved_addresses.front(), std::move(data),
                      traffic_annotation_, std::move(callback));
}

Neither path verifies if the target IP address is a multicast address when allow_multicast_ is set to false.

Potential Exploit Scenario

An attacker operating from a context that has direct socket access (such as an Isolated Web App or extension) but lacks the direct-sockets-multicast Permissions Policy could potentially execute the following steps:

  1. Open a bound UDP socket using navigator.directSockets.openBoundUDPSocket({ localAddress: '0.0.0.0' }).
  2. The browser validates the request and determines that multicast is not allowed for this context, passing allow_multicast = false to the Network Service. The Network Service instantiates a RestrictedUDPSocket with allow_multicast_ = false.
  3. The page triggers a UDP SendTo request targeting either a multicast IP literal (e.g., 224.0.0.251 or ff02::fb on port 5353 for mDNS) or a domain name they control that resolves via DNS to a multicast IP address.
  4. Because RestrictedUDPSocket does not check allow_multicast_ in its send paths, the network process receives the destination and forwards the raw UDP payload onto the local network link.

This would permit active local network discovery and spoofing (e.g., mDNS or SSDP spoofing) to discover local services on the user’s LAN, despite the permissions policy denying multicast access.

Note: Since our testing tools cannot currently execute code or launch real browsers, these steps are based on a static code review and are potential/suggested steps rather than a run proof-of-concept.

Suggested Fix

Ensure that both the fast-path direct IP destination and the DNS-resolved destinations are checked against the allow_multicast_ bit before transmitting. If a multicast address is detected but allow_multicast_ is false, terminate the connection and return a net error:

In RestrictedUDPSocket::SendTo:

  if (net::IPAddress address; address.AssignFromIPLiteral(dest_addr.host())) {
    if (address.IsMulticast() && !allow_multicast_) {
      std::move(callback).Run(net::ERR_ACCESS_DENIED);
      mojo::ReportBadMessage("no permission to use multicast");
      return;
    }
    udp_socket_->SendTo(net::IPEndPoint(std::move(address), dest_addr.port()),
                        data, traffic_annotation_, std::move(callback));
    return;
  }

In RestrictedUDPSocket::OnResolveCompleteForSendTo:

  if (resolved_addresses.front().address().IsMulticast() && !allow_multicast_) {
    std::move(callback).Run(net::ERR_ACCESS_DENIED);
    mojo::ReportBadMessage("no permission to use multicast");
    return;
  }

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


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