CVE-2026-79099
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/direct_sockets/direct_sockets_apitest.cc |
modified |
Files Changed
chrome/browser/about_flags.ccchrome/browser/direct_sockets/direct_sockets_apitest.ccchrome/browser/flag-metadata.json
Patch
From 405665065117459f6a72161b865c8244742b21c7 Mon Sep 17 00:00:00 2001
From: Tsuyoshi Horo <horo@chromium.org>
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 <horo@chromium.org>
Reviewed-by: Vlad Krot <vkrot@google.com>
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",
Regression Test / PoC
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)
Original Bug Report
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.ccservices/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:
- Open a bound UDP socket using
navigator.directSockets.openBoundUDPSocket({ localAddress: '0.0.0.0' }). - The browser validates the request and determines that multicast is not allowed for this context, passing
allow_multicast = falseto the Network Service. The Network Service instantiates aRestrictedUDPSocketwithallow_multicast_ = false. - The page triggers a UDP
SendTorequest targeting either a multicast IP literal (e.g.,224.0.0.251orff02::fbon port5353for mDNS) or a domain name they control that resolves via DNS to a multicast IP address. - Because
RestrictedUDPSocketdoes not checkallow_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.