MCP Server Security · document.domain · SOP Relaxation · Origin-Agent-Cluster

MCP server document.domain security — same-origin policy relaxation via document.domain mutation, subdomain DOM access, and Origin-Agent-Cluster isolation in MCP browser UIs

document.domain mutation is a legacy same-origin policy escape hatch that allows two documents to read and write each other's DOM if both share the same eTLD+1 and both set document.domain to that shared value. In MCP server deployments where the client UI runs on app.skillaudit.dev and other services run on sibling subdomains, a single XSS on any subdomain that sets document.domain = 'skillaudit.dev' gains full DOM access to every other subdomain that also set it — including reading input field values, intercepting form submissions, and injecting scripts directly into those pages. The Origin-Agent-Cluster: ?1 HTTP header prevents document.domain relaxation by opting a document into an isolated agent cluster where the setter is disabled entirely.

How document.domain relaxation works

The same-origin policy (SOP) normally requires scheme + host + port to match exactly. document.domain is a mutation that allows relaxation: if two documents on a.skillaudit.dev and b.skillaudit.dev both set document.domain = 'skillaudit.dev', the browser treats them as same-origin for DOM access purposes — each can call iframeElement.contentDocument and read or write the other's DOM tree.

// Page on a.skillaudit.dev
document.domain = 'skillaudit.dev';

// Page on b.skillaudit.dev (loaded in an iframe on a.skillaudit.dev)
document.domain = 'skillaudit.dev';

// Now a.skillaudit.dev CAN access the iframe's DOM:
const iframe = document.getElementById('b-subdomain-iframe');
const inputValue = iframe.contentDocument.getElementById('api-key-input').value; // ← works

The critical constraint: document.domain can only be set to the current domain or a parent domain up to the eTLD+1. skillaudit.dev is the eTLD+1; setting it to dev (the eTLD) throws SecurityError. Setting to a completely different eTLD+1 also throws. And both documents must set the value — one-sided setting is insufficient for relaxation.

The lifetime vulnerability: once set, cannot be unset in a browser context

Setting document.domain on a document is permanent for that browsing context's lifetime. There is no way for the application to "take back" the relaxation after setting it. The legacy API document.domain = '' (setting to empty string) does not restore strict same-origin checking — it throws in modern browsers or has no security effect. The only correct mitigation is the Origin-Agent-Cluster header, which prevents the setter from working at all:

// This does NOT restore strict origin checking — legacy behavior, now throws in Chrome 106+
document.domain = '';  // SecurityError or no-op depending on browser version

// CORRECT: prevent document.domain relaxation at the HTTP response header level
// Caddy configuration:
// header Origin-Agent-Cluster "?1"

// nginx configuration:
// add_header Origin-Agent-Cluster "?1" always;

// Express.js:
app.use((req, res, next) => {
  res.setHeader('Origin-Agent-Cluster', '?1');
  next();
});

Setting document.domain on any one tab makes the entire origin vulnerable for that tab's lifetime. If a long-lived MCP client tab sets document.domain for any reason (e.g., to integrate with a legacy subdomain widget), every attacker-controlled subdomain that also sets it can intrude into the MCP client's DOM — reading session tokens, injecting login forms, or exfiltrating rendered tool output — for as long as the tab remains open. There is no partial relaxation: it is all-or-nothing for the pair of documents that set it.

MCP tool output attack vector

If an MCP tool output rendering context is on app.skillaudit.dev and tool output contains a script that sets document.domain = 'skillaudit.dev', and there is any legitimate page that also uses the pattern (or an attacker controls a subdomain like cdn.skillaudit.dev), the attacker context gains DOM access to the MCP client:

// Malicious tool output script sets document.domain to enable cross-subdomain DOM access
document.domain = 'skillaudit.dev';

// If any legitimate subdomain also sets document.domain,
// the attacker context can now load an iframe pointing to that subdomain
// and read its DOM once it also sets document.domain

// More direct vector: if the MCP UI itself set document.domain (legacy code),
// the tool output script on the same page already has full DOM access via
// normal same-origin JS — but also inherits the relaxation, enabling
// cross-tab DOM access if the tool output opens a new window on the same origin

Origin-Agent-Cluster: the correct defense

The Origin-Agent-Cluster: ?1 response header (a structured field boolean) opts a document into an origin-keyed agent cluster. In an origin-keyed agent cluster, document.domain mutation is disabled — the setter throws or is silently ignored. The page is isolated to its own agent cluster and cannot be joined with other documents via domain relaxation:

ScenarioWithout Origin-Agent-ClusterWith Origin-Agent-Cluster: ?1
document.domain = 'example.com'Succeeds; SOP relaxation active for window lifetimeThrows SecurityError; setter is disabled
Cross-subdomain iframe DOM accessPossible if both set document.domainNot possible; origin-keyed isolation enforced
Tool output script sets document.domainApplies to the page's browsing contextIgnored; no effect on SOP
Cross-Origin-Isolation (SharedArrayBuffer)Not impliedNot implied; COEP + COOP still required separately

Origin-Agent-Cluster: ?1 does not enable cross-origin isolation — it does not grant access to SharedArrayBuffer or high-resolution timers. Those require Cross-Origin-Embedder-Policy: require-corp combined with Cross-Origin-Opener-Policy: same-origin. Origin-Agent-Cluster is solely a document.domain relaxation prevention mechanism.

SkillAudit findings for document.domain vulnerabilities in MCP server UIs

CRITICAL −22MCP client page sets document.domain for legacy subdomain integration — any attacker-controlled subdomain (or successful XSS on any sibling subdomain) that also sets document.domain can read and write the MCP client's DOM tree, including session tokens, API key inputs, and rendered tool output
HIGH −18Tool output rendering context (iframe or same-page) can set document.domain (no CSP script-src to block inline scripts) — if any sibling subdomain uses document.domain, the attacker context joins the relaxation and gains access to those subdomains' DOM
HIGH −16Missing Origin-Agent-Cluster: ?1 header on all MCP server responses — document.domain mutation is permitted by the browser and not opted out of; the page is vulnerable to document.domain-based cross-subdomain attacks if any subdomain in the eTLD+1 uses the pattern
MEDIUM −10Origin-Agent-Cluster header absent on error pages and redirect responses (missing always flag in nginx) — 4xx/5xx pages rendered without the header are in the default site-keyed agent cluster and inherit document.domain relaxation

See also: XSS security · iframe sandbox security · CORS security

Run a free SkillAudit on your MCP server →