Sandboxing and Trusted Types

Sandboxed iframes and Trusted Types

The sandbox attribute

An empty sandbox attribute on an <iframe> (demonstrated in <iframe>) gives the frame an opaque origin and blocks scripts, forms, popups, modal dialogs, downloads and top-level navigation. Each token lifts one restriction:

Common iframe sandbox tokens
Token Re-enables
allow-scripts JavaScript execution
allow-same-origin The real origin: cookies, storage, same-origin access
allow-forms Form submission
allow-popups, allow-modals, allow-downloads New windows; alert(); downloads
allow-top-navigation-by-user-activation Navigating the top page after a click

Also defined are allow-popups-to-escape-sandbox, allow-pointer-lock, allow-orientation-lock, allow-presentation, allow-top-navigation and allow-top-navigation-to-custom-protocols. Grant the minimum: a code playground needs sandbox="allow-scripts" on a separate origin, never allow-scripts allow-same-origin on your own, which lets the frame remove its sandbox. For files you serve rather than frame, such as user uploads, the CSP header Content-Security-Policy: sandbox applies the same restrictions to the whole document.

Trusted Types

Most modern XSS is DOM-based: your own JavaScript passes attacker-controlled text to an injection sink such as innerHTML, document.write(), eval() or a script's src. Trusted Types, Baseline since February 2026 (Chrome 83 1 , Firefox 148 555 , Safari 26 10 ), makes those sinks refuse plain strings once the CSP directive require-trusted-types-for 'script' is present. Code must pass a TrustedHTML, TrustedScript or TrustedScriptURL object created by a named policy, and the trusted-types directive lists which policy names may exist. Security review then shrinks from every innerHTML in the codebase to the few policy functions.

Strings rejected, policy output acceptedHTMLLive
<!doctype html>
<meta http-equiv="Content-Security-Policy"
  content="require-trusted-types-for 'script'; trusted-types comments">
<ul id="log" style="font: 15px system-ui"></ul>
<script>
  const say = (t) => log.append(Object.assign(document.createElement('li'), { textContent: t }));
  const input = 'Nice post! <img src=x onerror="alert(document.cookie)">';
  const box = document.createElement('p');
  try { box.innerHTML = input; } catch (e) { say(`Plain string: ${e.name}`); }
  const policy = trustedTypes.createPolicy('comments', {
    createHTML: (s) => s.replaceAll('&', '&amp;').replaceAll('<', '&lt;'),
  });
  box.innerHTML = policy.createHTML(input);
  say(`Policy output accepted, rendered as text: ${box.textContent}`);
  try { trustedTypes.createPolicy('other', {}); } catch (e) { say(`Unlisted policy: ${e.name}`); }
</script>
Browser output of Listing 2.50
Browser output of 50

Real policies usually sanitize instead of escaping. DOMPurify 17,413 (https://github.com/cure53/DOMPurify 17,413 ) (Apache-2.0 or MPL-2.0, npm 2,036 install dompurify) parses the markup, removes scripts and event handlers, and with { RETURN_TRUSTED_TYPE: true } returns TrustedHTML directly. A policy named default is called automatically whenever a plain string reaches a sink, which helps migrate legacy code. Deploy it like CSP: start with Content-Security-Policy-Report-Only: require-trusted-types-for 'script' and fix each reported sink.