Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 58 additions & 4 deletions index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -1883,11 +1883,65 @@ This creates a personalization-to-fingerprinting pipeline where sites can extrac
<li><strong>Discrimination risk</strong>: Extracted attributes (age, pregnancy status, location) could be used for price discrimination or biased service</li>
</ul>

<h4 id="violation-same-origin-boundaries">Violation of Same-Origin Boundaries</h4>
<h4 id="interaction-with-same-origin-policy">Interaction with the Same-Origin Policy</h4>

<p class="XXX">
TODO: Document risks and implications of [=agents=] carrying state from one origin to another. Detail how tools executed on one origin may carry state from another origin, potentially leading to data leakage or same-origin policy bypasses if not handled securely by the [=user agent=]. This section should probably talk about the WebMCP permissions policy and other cross-origin opt in mechanisms.
</p>
The Same-Origin Policy (SOP) is the foundational security boundary of the web platform, isolating documents of different [=origins=] so that one [=origin=] cannot inspect or manipulate the state of another without explicit consent.

In WebMCP, a registered tool's {{ModelContextTool/execute}} callback always runs within the registering {{Document}}'s execution context, with full access to that [=origin=]'s DOM, storage, and ambient credentials. Consequently, allowing a cross-origin {{Document}} in the frame tree to discover a tool ({{ModelContext/getTools()}}), invoke it ({{ModelContext/executeTool()}}), and receive its serialized return value crosses a privilege boundary. Without strict enforcement, a malicious embedding page or embedded subframe could silently invoke privileged operations or extract sensitive state from another [=origin=].

<h5 id="in-page-cross-origin-exposure">Enforcing Same-Origin Isolation via Two-Sided Opt-In</h5>

By default, WebMCP enforces strict [=same origin=] isolation: a tool registered via <code><var ignore>document</var>.{{Document/modelContext}}.{{ModelContext/registerTool()}}</code> is only visible to and executable by {{Document}}s that are [=same origin=] with the registering {{Document}} (tool owner). Cross-origin {{Document}}s cannot discover, observe {{ModelContext/toolchange}} events for, or execute a tool unless three independent access-control gates are satisfied:

<ol>
<li>
<strong>Embedder Delegation via Permissions Policy (<code>allow="tools"</code>)</strong>: Access to all WebMCP APIs ({{ModelContext/registerTool()}}, {{ModelContext/getTools()}}, and {{ModelContext/executeTool()}}) is gated behind the "{{tools}}" [=policy-controlled feature=], whose [=policy-controlled feature/default allowlist=] is <code>[=default allowlist/'self'=]</code>. Consequently, a cross-origin <{iframe}> cannot register or expose tools—nor will an ancestor's {{ModelContext/getTools()}} traversal inspect that subframe—unless the embedding {{Document}} explicitly delegates the "{{tools}}" feature to the subframe via <code>&lt;iframe allow="tools"&gt;</code>.
</li>
<li>
<strong>Tool Provider Opt-In ({{ModelContextRegisterToolOptions/exposedTo}})</strong>: A {{Document}} registering a tool must explicitly opt in to cross-origin exposure by specifying the permitted caller [=origins=] in {{ModelContextRegisterToolOptions/exposedTo}}. Each listed [=origin=] must be a valid, [$is origin potentially trustworthy?|potentially trustworthy$] [=origin=]. During both discovery ({{ModelContext/getTools()}}) and invocation ({{ModelContext/executeTool()}}), the [=user agent=] evaluates the [=tool is exposed to an origin=] algorithm, ensuring that only callers whose [=Document/origin=] is [=same origin=] with the tool owner or explicitly listed in {{ModelContextRegisterToolOptions/exposedTo}} can access the tool.
</li>
<li>
<strong>Tool Consumer Opt-In ({{ModelContextGetToolOptions/fromOrigins}})</strong>: Even when an embedded cross-origin {{Document}} has been delegated <code>allow="tools"</code> and has exposed a tool to its parent [=origin=] via {{ModelContextRegisterToolOptions/exposedTo}}, {{ModelContext/getTools()}} only returns tools from [=same origin=] {{Document}}s by default. To discover tools from a cross-origin descendant [=navigable=], the calling {{Document}} must also explicitly list the target [=origin=] in {{ModelContextGetToolOptions/fromOrigins}}.
</li>
</ol>

**Why Two-Sided Opt-In Matters**: Requiring mutual consent from both the tool provider ({{ModelContextRegisterToolOptions/exposedTo}}) and the tool consumer ({{ModelContextGetToolOptions/fromOrigins}}) prevents both unauthorized cross-origin actuation and unsolicited tool-list pollution. Without provider opt-in ({{ModelContextRegisterToolOptions/exposedTo}}), a malicious parent page could invoke internal tools inside an embedded third-party widget that were intended only for [=same origin=] use. Conversely, without consumer opt-in ({{ModelContextGetToolOptions/fromOrigins}}), a compromised or untrusted third-party <{iframe}> could unilaterally expose tools with deceptive names or prompt-injected descriptions ([[#metadata-description-attacks]]) into the parent page's {{ModelContext/getTools()}} results.

```js
// 1. Parent document (https://app.example.com) explicitly delegates the "tools"
// feature to the embedded widget (<iframe src="https://widget.example.org" allow="tools">)
// and explicitly requests tools from that origin via `fromOrigins`:
const tools = await document.modelContext.getTools({
fromOrigins: ["https://widget.example.org"]
});

// 2. Child iframe (https://widget.example.org) explicitly exposes the tool
// to the parent origin via `exposedTo`:
await document.modelContext.registerTool(
{
name: "generate-chart",
description: "Renders a chart inside the widget",
inputSchema: { type: "object", properties: { data: { type: "array" } } },
execute: async ({ data }) => { /* ... */ }
},
{
exposedTo: ["https://app.example.com"]
}
);
```

<h5 id="same-origin-residual-risks">Residual Risks</h5>

Even when cross-origin access is governed by the mechanisms above, two categories of residual risk remain:

<ul>
<li>
<strong>Direct Cross-Origin Exposure</strong>: Even when two [=origins=] mutually opt in via {{ModelContextRegisterToolOptions/exposedTo}} and {{ModelContextGetToolOptions/fromOrigins}}, they remain separate trust domains: tool providers must still treat caller-supplied arguments as untrusted input, and callers must treat cross-origin tool descriptions and return values as untrusted data (see [[#prompt-injection]]). Additionally, because {{ModelContext/getTools()}} aggregates tools across all descendant frames at the [=origin=] level, callers should inspect {{RegisteredTool/origin}} and {{RegisteredTool/window}} on each {{RegisteredTool}} to disambiguate tools when multiple subframes or [=origins=] expose tools with the same {{RegisteredTool/name}}.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good callout.

</li>
<li>
<strong>Agent-Mediated Cross-Origin State Transfer</strong>: Where an external or browser-integrated [=agent=] ([=browser's agent=]) interacts with multiple [=origins=] over the course of a task and carries state derived from one [=origin=] into tool invocations on another [=origin=]. This is a general risk of agentic browsing across the web rather than a WebMCP-specific threat.
</li>
</ul>

<h4 id="interaction-with-private-browsing">Interaction with Private Browsing Modes</h4>

Expand Down
Loading