Chrome · WebMCP
CVE-2026-87521
Logic Error in WebMCP
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
CorsExploitBrowserTestcontent/browser/security_exploit_browsertest.cc |
modified |
Files Changed
content/browser/renderer_host/model_context_user_data.cccontent/browser/security_exploit_browsertest.cc
Patch
From b4983713ccc6cc723c7bf3d5c5228dd52f7ce9b1 Mon Sep 17 00:00:00 2001
From: François Beaufort <beaufort.francois@gmail.com>
Date: Thu, 30 Jul 2026 23:51:18 -0700
Subject: [PATCH] [WebMCP] clear exposed_origins to prevent leakage to renderers
Bug: 512986143, 512533939
Change-Id: I67bc4e762503bd73fde091f71c418455a0291f00
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8137431
Reviewed-by: Dominic Farolino <dom@chromium.org>
Commit-Queue: Fr <beaufort.francois@gmail.com>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1671656}
---
diff --git a/content/browser/renderer_host/model_context_user_data.cc b/content/browser/renderer_host/model_context_user_data.cc
index dcbce4ed..965a68f 100644
--- a/content/browser/renderer_host/model_context_user_data.cc
+++ b/content/browser/renderer_host/model_context_user_data.cc
@@ -240,6 +240,11 @@
->GetFrameTokenForSiteInstanceGroup(site_instance_group);
cloned_tool->tool_owner_frame_token = token;
+
+ // Clear `exposed_origins` to prevent leaking the full list of
+ // authorized origins to the calling renderer process.
+ cloned_tool->exposed_origins.clear();
+
all_tools.push_back(std::move(cloned_tool));
}
return RenderFrameHost::FrameIterationAction::kContinue;
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index c1d86a44..9909662 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -906,6 +906,52 @@
EXPECT_EQ(bad_message::RFHI_WEBMCP_OPAQUE_TARGET_ORIGIN, kill_waiter.Wait());
}
+// Test that `GetScriptTools()` clears `exposed_origins` so that the full ACL
+// list is not leaked to caller renderers.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPEnabled,
+ GetScriptToolsExposedOriginsRedacted) {
+ GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+ EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+ RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
+ shell()->web_contents()->GetPrimaryMainFrame());
+
+ EXPECT_TRUE(
+ rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));
+
+ auto tool = blink::mojom::ScriptTool::New();
+ tool->name = "test-tool";
+ tool->description = "test description";
+ tool->tool_owner_frame_token = rfh->GetFrameToken();
+ tool->origin = rfh->GetLastCommittedOrigin();
+ tool->exposed_origins.push_back(
+ url::Origin::Create(GURL("https://friend.example")));
+ tool->exposed_origins.push_back(
+ url::Origin::Create(GURL("https://attacker.example")));
+
+ mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
+ rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());
+
+ base::RunLoop register_loop;
+ script_tool_host->RegisterScriptTool(std::move(tool),
+ register_loop.QuitClosure());
+ register_loop.Run();
+
+ base::RunLoop get_tools_loop;
+ std::vector<blink::mojom::ScriptToolPtr> result_tools;
+ script_tool_host->GetScriptTools(
+ {}, base::BindLambdaForTesting(
+ [&](std::vector<blink::mojom::ScriptToolPtr> tools) {
+ result_tools = std::move(tools);
+ get_tools_loop.Quit();
+ }));
+ get_tools_loop.Run();
+
+ ASSERT_EQ(1u, result_tools.size());
+ EXPECT_EQ("test-tool", result_tools[0]->name);
+ EXPECT_TRUE(result_tools[0]->exposed_origins.empty());
+}
+
class CorsExploitBrowserTest : public ContentBrowserTest {
public:
CorsExploitBrowserTest() = default;
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index c1d86a44..9909662 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -906,6 +906,52 @@
EXPECT_EQ(bad_message::RFHI_WEBMCP_OPAQUE_TARGET_ORIGIN, kill_waiter.Wait());
}
+// Test that `GetScriptTools()` clears `exposed_origins` so that the full ACL
+// list is not leaked to caller renderers.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPEnabled,
+ GetScriptToolsExposedOriginsRedacted) {
+ GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+ EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+ RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
+ shell()->web_contents()->GetPrimaryMainFrame());
+
+ EXPECT_TRUE(
+ rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));
+
+ auto tool = blink::mojom::ScriptTool::New();
+ tool->name = "test-tool";
+ tool->description = "test description";
+ tool->tool_owner_frame_token = rfh->GetFrameToken();
+ tool->origin = rfh->GetLastCommittedOrigin();
+ tool->exposed_origins.push_back(
+ url::Origin::Create(GURL("https://friend.example")));
+ tool->exposed_origins.push_back(
+ url::Origin::Create(GURL("https://attacker.example")));
+
+ mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
+ rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());
+
+ base::RunLoop register_loop;
+ script_tool_host->RegisterScriptTool(std::move(tool),
+ register_loop.QuitClosure());
+ register_loop.Run();
+
+ base::RunLoop get_tools_loop;
+ std::vector<blink::mojom::ScriptToolPtr> result_tools;
+ script_tool_host->GetScriptTools(
+ {}, base::BindLambdaForTesting(
+ [&](std::vector<blink::mojom::ScriptToolPtr> tools) {
+ result_tools = std::move(tools);
+ get_tools_loop.Quit();
+ }));
+ get_tools_loop.Run();
+
+ ASSERT_EQ(1u, result_tools.size());
+ EXPECT_EQ("test-tool", result_tools[0]->name);
+ EXPECT_TRUE(result_tools[0]->exposed_origins.empty());
+}
+
class CorsExploitBrowserTest : public ContentBrowserTest {
public:
CorsExploitBrowserTest() = default;
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page