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:
| 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.
<!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('&', '&').replaceAll('<', '<'),
});
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>
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.