Medium watch CVSS 3.1 chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in WebMCP
DescriptionInformation leak in WebMCP
ComponentWebMCP
Sub-componentWebMCP
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker512986143
Fix commitb4983713ccc6 (chromium/src) +51/-0
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-13 (per vendor advisory)
Fix landed2026-07-31 (public git)
Disclosed2026-09-08
Patch gap39 days — fix public in source before the advisory

Background

WebMCP
the Chrome feature (component WebMCP) that lets a page register script-backed “tools” a model can invoke, exposed to the browser process via the blink::mojom::ModelContextHost Mojo interface.
`ScriptTool`
the blink::mojom::ScriptTool struct describing a registered tool, carrying fields such as name, description, origin, tool_owner_frame_token, and exposed_origins.
`exposed_origins`
the list of url::Origin values on a ScriptTool naming which origins are authorized to see/use the tool, i.e. a server-side access-control list (ACL) that should not be handed back to a calling renderer.
Renderer/browser trust boundary
data returned from the privileged browser process to a lower-privileged renderer over Mojo (here, the GetScriptTools() reply) crosses a trust boundary and must be redacted of anything the renderer is not entitled to learn.

Root Cause Analysis

When the browser process assembled the list of tools to return to a caller, it cloned each stored ScriptTool and set the clone’s tool_owner_frame_token from GetFrameTokenForSiteInstanceGroup(site_instance_group), then pushed the clone onto all_tools and returned it to the calling renderer. The cloned tool retained its exposed_origins field, so the full authorized-origin ACL of every tool was serialized back across the browser-to-renderer boundary. This violated the invariant that a renderer should only receive data pertinent to itself and not the complete access-control list of other origins.

The fix adds cloned_tool->exposed_origins.clear(); immediately after the frame-token assignment and before all_tools.push_back(std::move(cloned_tool)), so the clone returned to the renderer carries an empty exposed_origins, while the authoritative copy the browser retains is unaffected. The accompanying browser test GetScriptToolsExposedOriginsRedacted confirms that after registering a tool with two exposed_origins entries, GetScriptTools() returns a tool whose exposed_origins is empty.

Key insight
The core mistake was returning a clone of the internal ScriptTool to the renderer without stripping its server-side ACL field, leaking exposed_origins across the trust boundary; the fix simply clears exposed_origins on the clone before it is pushed and returned.

Attack Path

  1. Register a tool A page in a renderer binds ModelContextHost and calls RegisterScriptTool with a ScriptTool whose exposed_origins lists other authorized origins, which the browser stores.
  2. Request the tool list The same (or another) renderer calls GetScriptTools() over the same Mojo interface, prompting the browser process to clone each stored tool for the reply.
  3. Receive the un-redacted ACL Before the patch, each cloned tool was pushed onto all_tools with exposed_origins intact, so the reply delivered to the calling renderer contained the full list of authorized origins for tools it should not have full visibility into.
  4. Harvest authorized origins The renderer reads the returned exposed_origins to learn which origins are authorized for each tool, i.e. information disclosure of the ACL contents (inferred use of the leaked data; the patch demonstrates only the leak, not any further escalation).

Impact Assessment

An attacker in a renderer process gains read access to the exposed_origins access-control lists of registered WebMCP tools — the set of authorized origins — which they are not entitled to enumerate; the advisory classifies this as an information leak (InfoDisclosure) in WebMCP. The precondition is that WebMCP is enabled and reachable (the test guards on PermissionsPolicyFeature::kTools) and that the renderer can invoke GetScriptTools(). The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 3.1. The advisory does not report exploited-in-the-wild status or a specific severity, and the patch demonstrates only the leak, not any further escalation.

Changed Functions

FunctionChangeNotes
CorsExploitBrowserTest
content/browser/security_exploit_browsertest.cc
modified

Files Changed

  • content/browser/renderer_host/model_context_user_data.cc
  • content/browser/security_exploit_browsertest.cc

Audit Directions

  • Cloned-object leakage across IPC
    Wherever the browser process clones an internal struct (e.g. a ScriptTool) and returns it to a renderer, audit that every server-only or ACL field is cleared before serialization, since fields left populated leak by default.
  • ACL and origin-list fields
    Search WebMCP and related Mojo replies for fields that hold authorization data (exposed_origins, allowlists, tokens) to confirm they are stripped from renderer-facing responses rather than passed through.
  • Redact-on-return invariant
    Review GetScriptTools() and sibling accessors on ModelContextHost (and similar mojom interfaces) to verify each output path applies redaction between the internal representation and the value handed back over the boundary.
From b4983713ccc6cc723c7bf3d5c5228dd52f7ce9b1 Mon Sep 17 00:00:00 2001
From: François Beaufort <[email protected]>
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 <[email protected]>
Commit-Queue: Fr <[email protected]>
Reviewed-by: Alex Moshchuk <[email protected]>
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

reported by [email protected]

Cross-origin metadata leak in WebMCP GetScriptTools implementation

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The browser-side implementation of the WebMCP (Web Model Control Protocol) feature potentially leaks the full Access Control List (ACL) of a script tool to any origin authorized to discover it. This allows a compromised renderer to identify all other origins that a victim has authorized to use a specific script tool.

Affected files:

  • content/browser/renderer_host/model_context_user_data.cc
  • third_party/blink/public/mojom/content_extraction/script_tools.mojom
  • third_party/blink/renderer/core/script_tools/model_context.cc

Estimated timestamp from git blame: 2026-05-01

Summary

A potential logic error in the browser process handling of WebMCP script tool discovery allows an authorized renderer to learn the identity of all other origins authorized to access a given tool. This constitutes a cross-origin information leak of metadata, revealing a victim origin’s trust relationships.

Root Cause

In content/browser/renderer_host/model_context_user_data.cc, the GetScriptTools method handles Mojo requests from renderers to discover available script tools. When a tool is visible to the caller (based on the caller’s origin being present in the tool’s exposed_origins list), the browser process clones the stored blink::mojom::ScriptTool struct and returns it to the caller.

// content/browser/renderer_host/model_context_user_data.cc:212
if (IsScriptToolVisibleToOrigin(rfh->GetLastCommittedOrigin(),
                                t->exposed_origins, caller_origin)) {
  blink::mojom::ScriptToolPtr cloned_tool = t.Clone();
  // ... redaction of other fields occurs here, but exposed_origins is missed
  cloned_tool->tool_owner_frame_token = token;
  all_tools.push_back(std::move(cloned_tool));
}

The blink::mojom::ScriptTool struct (third_party/blink/public/mojom/content_extraction/script_tools.mojom) contains the exposed_origins field, which is an array of all origins authorized to access the tool. The browser process fails to redact this array from the cloned struct before sending it back to the renderer.

Potential Impact

An attacker who has compromised a renderer process (e.g., via a renderer RCE) can discover the identity of other origins that a victim site has authorized for tool access. This reveals sensitive cross-origin relationship metadata across the Site-Isolation boundary.

Suggested Reproduction Steps

  1. A victim page (e.g., https://victim.example) registers a script tool and authorizes access for https://attacker.example and https://friend.example using the exposedTo option in the WebMCP API.
  2. The attacker-controlled origin (https://attacker.example) is loaded in a frame where the tools permissions-policy is enabled.
  3. The attacker compromises the renderer process for https://attacker.example.
  4. From the compromised renderer, the attacker invokes the Mojo method ModelContextHost::GetScriptTools() via the BrowserInterfaceBroker.
  5. The attacker inspects the exposed_origins field in the received Mojo response, discovering that https://friend.example is also authorized for the victim’s tool.

Suggested Fix

Modify ModelContextUserData::GetScriptTools in content/browser/renderer_host/model_context_user_data.cc to clear the exposed_origins list in the ScriptTool struct before it is sent to the renderer process. The renderer only needs to know that it has access, not which other origins also have access.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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