Security Guide
MCP server Trusted Types security — DOM XSS prevention via trustedTypes.createPolicy(), require-trusted-types-for 'script' CSP, DOMPurify 3.x integration, and innerHTML sink enforcement
Trusted Types is a browser security API that prevents DOM-based XSS by requiring JavaScript to pass string values through a named policy before assigning them to dangerous DOM sinks: innerHTML, outerHTML, insertAdjacentHTML(), document.write(), eval(), and more. Without Trusted Types, any string (including MCP tool output) can be assigned directly to innerHTML — and if that string contains script tags or event handlers that survived sanitization, XSS executes. With Trusted Types and the CSP directive require-trusted-types-for 'script', a string assigned to innerHTML without first passing through a registered policy throws a TypeError immediately — the assignment never reaches the DOM parser.
The problem Trusted Types solves
DOM-based XSS occurs when data flows from an untrustworthy source (tool output, URL parameters, postMessage) into a dangerous DOM sink (innerHTML, eval(), src=) without adequate sanitization. In MCP server clients, the dominant pattern is:
// Common (and dangerous) pattern in MCP tool output rendering
const toolOutput = await mcp.callTool('search', { query });
resultDiv.innerHTML = toolOutput.content; // Direct innerHTML assignment — XSS vector
DOMPurify is typically added as a mitigation, but the fix is applied by the developer voluntarily — there is nothing at the language or browser level that prevents forgetting it, or prevents a future code change from bypassing it. Trusted Types changes this: with the CSP enforcement directive active, the above code throws a TypeError because toolOutput.content is a plain string, not a TrustedHTML object. The assignment is rejected before it reaches the HTML parser.
How Trusted Types work
Trusted Types introduces a set of typed wrapper objects (TrustedHTML, TrustedScript, TrustedScriptURL) and a policy creation API (trustedTypes.createPolicy()). A policy defines how strings are transformed into trusted objects. The browser enforces that only TrustedHTML objects (not plain strings) can be assigned to innerHTML:
// Create a sanitization policy using DOMPurify
const sanitizePolicy = trustedTypes.createPolicy('dompurify-sanitize', {
createHTML: (dirty) => DOMPurify.sanitize(dirty, { RETURN_TRUSTED_TYPE: true })
});
// Now use the policy — returns a TrustedHTML object, not a string
const trusted = sanitizePolicy.createHTML(toolOutput.content);
resultDiv.innerHTML = trusted; // Accepted — trusted is TrustedHTML, not a plain string
// This would throw TypeError with require-trusted-types-for 'script':
// resultDiv.innerHTML = toolOutput.content; // BLOCKED — plain string rejected
DOMPurify 3.x native integration: DOMPurify 3.0+ integrates natively with Trusted Types via the RETURN_TRUSTED_TYPE: true option. When this option is set and Trusted Types is available in the browser, DOMPurify.sanitize() returns a TrustedHTML object instead of a string — making it directly assignable to innerHTML without any additional wrapper. DOMPurify also creates its own internal policy named dompurify that it uses for its own DOM manipulation during the sanitization process.
The CSP directive: require-trusted-types-for 'script'
Adding the Trusted Types policy API to your code without the CSP directive provides zero security guarantees — a developer or attacker can still assign plain strings to innerHTML and the browser won't block it. The enforcement comes from the CSP header:
Content-Security-Policy: require-trusted-types-for 'script';
With this directive active, the browser enforces at runtime that every assignment to a dangerous sink receives a typed object from a registered policy, not a plain string. A plain string assignment throws TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.
Trusted Types sink coverage
| Sink | Requires type | Policy method | Notes |
|---|---|---|---|
element.innerHTML | TrustedHTML | createHTML() | Most common MCP tool output render point |
element.outerHTML | TrustedHTML | createHTML() | Same HTML parser path as innerHTML |
element.insertAdjacentHTML() | TrustedHTML | createHTML() | Common in streaming tool output rendering |
document.write() | TrustedHTML | createHTML() | Deprecated but still blocked by TT |
eval() | TrustedScript | createScript() | Should never be used with tool output |
script.src | TrustedScriptURL | createScriptURL() | Prevents script URL injection |
element.setAttribute('onclick', ...) | TrustedScript | createScript() | Inline event handlers via setAttribute |
Default policy for migration
Enabling Trusted Types on an existing codebase can break third-party libraries that assign plain strings to innerHTML internally. The Trusted Types API provides a default policy as a migration mechanism — any plain string that reaches a sink without an explicit policy is passed through the default policy instead of throwing immediately:
// Migration aid: default policy that logs violations rather than blocking
trustedTypes.createPolicy('default', {
createHTML: (string) => {
console.warn('Untrusted HTML assignment intercepted:', string.substring(0, 100));
// During migration, pass through but log
return string;
// Once all code is updated, change to: throw new Error('Direct innerHTML prohibited');
}
});
The default policy is a migration tool, not a security control. A default policy that returns the string unchanged provides no XSS prevention — it just adds logging. SkillAudit flags the presence of a permissive default policy as MEDIUM severity because it indicates Trusted Types is deployed in report-only mode rather than enforcing mode, leaving all DOM XSS paths open.
SkillAudit findings for Trusted Types
innerHTML without Trusted Types enforcement. The require-trusted-types-for 'script' CSP directive is absent — no browser-level enforcement prevents a forgotten or bypassed DOMPurify call from passing raw tool output directly to the DOM parser. Score −18.trustedTypes.createPolicy() found) but the CSP require-trusted-types-for 'script' directive is absent. Policy code runs but the browser does not enforce that plain strings are rejected at sinks — Trusted Types provides no security without the CSP enforcement directive. Score −16.default policy is registered that passes all plain strings through without sanitization. Third-party library calls to innerHTML with raw strings are not blocked. Score −12.RETURN_TRUSTED_TYPE: true. The sanitized string is returned as a plain string, then assigned to innerHTML. If Trusted Types enforcement is enabled, this throws; if not, it works but the chain is not typed end-to-end. Score −10.Content-Security-Policy-Report-Only — violations are reported but not blocked. MCP tool output can still reach DOM sinks as plain strings without enforcement. Score −6.Run a SkillAudit scan to audit your MCP server client's Trusted Types posture. The scanner checks for the require-trusted-types-for 'script' CSP directive, identifies direct innerHTML assignments from tool output data flows, audits DOMPurify integration for RETURN_TRUSTED_TYPE usage, and flags permissive default policies.