cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Isolation & sandboxing

Run untrusted JavaScript, widgets, ads, and user content safely with iframe sandboxing, postMessage checks, COOP and COEP.

By the end, you can
  • 01
    Lock down embedded contentChoose iframe sandbox tokens deliberately and avoid the dangerous same-origin script pair.
  • 02
    Communicate across boundariesUse postMessage, targetOrigin, origin checks, source checks, shape checks, and MessageChannel safely.
  • 03
    Pick the right isolation layerCompare iframes, workers, cross-origin isolation, ShadowRealm, Node vm, and SES-style lockdown.

Isolation before trust

Professional JavaScript often touches code or content your team did not write: analytics tags, ads, payment widgets, browser extensions, customer HTML, plugin scripts, and learner code in a playground. The safe question is not “do we trust it?” The safer question is “what boundary keeps it from reaching anything else?”

Plain definition

Isolation means running untrusted code or content behind a boundary that grants only the powers it needs, then communicating through a small validated channel instead of sharing the whole page.

This lesson builds on windows and frames, messaging and structured cloning, eval, and XSS. We will recap the same-origin policy from CORS, then add iframe sandboxing, message validation, cross-origin isolation headers, workers, ShadowRealm, and Node vm trade-offs.

Real-life analogyA locked room for guest work

A guest can work in a locked room without touching the rest of the house. Browser isolation runs risky code in a smaller place and passes checked messages through a small door.

In real life: A separate locked room
In JavaScript: A sandboxed iframe or process boundary
In real life: A small door for messages
In JavaScript: A narrow postMessage or MessagePort API
In real life: Rules for what enters
In JavaScript: Sandbox tokens and validation

Where the analogy stops: A real room has a physical lock. Software isolation still depends on browser behavior, added permissions, and careful message checks.

Boundaries and origins

The same-origin policy is the baseline boundary. Two documents are same-origin only when scheme, host, and port match. Same-origin documents can usually read each other; cross-origin documents usually cannot read DOM, storage, cookies, or variables. The CORS lesson is about network reads. Here the question is what a framed document or script can do once it is already on the page.

Third-party widgets and ads need isolation because they are still code in a user’s browser. A map widget should not read your CSRF token. A customer HTML preview should not reach your admin app. A plugin should not freeze the UI forever. A code editor, including this site’s own editor, should let examples run without giving them the completefrontend.com origin.

Isolation is not one feature

Use layers. Sandboxed iframes isolate documents. Workers isolate DOM access and main thread work. COOP and COEP isolate the browsing context group for powerful memory APIs. Message validation isolates intent. None of them excuses unsafe rendering, token storage mistakes, or the defenses in CSRF and tokens.

The iframe sandbox

BROWSER LAB

A bare sandbox attribute starts by removing powers: scripts, forms, popups, top navigation, pointer lock, downloads, modal dialogs, and the frame’s normal same-origin identity. Then you add back only the tokens the embed needs. Common tokens include allow-scripts, allow-forms, allow-popups, and allow-same-origin.

The dangerous pair

Never rely on sandbox="allow-scripts allow-same-origin" to isolate same-origin untrusted content. The framed page can run script while being treated as your origin. For same-origin content, that can let it remove its own sandbox attribute.

Step through sandbox tokens
Step 0 of 7Ready
Your turn: follow the blue line

Pick a sandbox token set, then step through which browser powers are still blocked.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
 function describeSandbox(tokens, sameOriginContent) {  const has = (name) => tokens.includes(name);  return {    scripts: has("allow-scripts") ? "allowed" : "blocked",    forms: has("allow-forms") ? "allowed" : "blocked",    popups: has("allow-popups") ? "allowed" : "blocked",    origin: has("allow-same-origin") ? "real origin" : "opaque origin",    topNavigation: has("allow-top-navigation") ? "allowed" : "blocked",    canRemoveSandbox: sameOriginContent &&      has("allow-scripts") && has("allow-same-origin"),  };} console.log(describeSandbox(tokens, true));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose a token set
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The runnable snippet below creates a real sandboxed iframe in the lesson editor’s page. It allows scripts so the child can report back, but it never grants allow-same-origin. The child’s direct attempt to read the parent DOM is caught as a security error, and the message origin is the opaque string "null".

Create a sandboxed iframe and read its reportPop out in the code editor (opens in a new tab)JavaScript
const frame = document.createElement("iframe");frame.sandbox = "allow-scripts";frame.srcdoc = `<!doctype html><script>let parentDom = "read";try { parent.document.body; } catch (error) { parentDom = "blocked:" + error.name; }let cookie = "";try { document.cookie = "lesson=1"; cookie = document.cookie || "(empty)"; }catch (error) { cookie = "blocked:" + error.name; }parent.postMessage({ lesson: "sandbox-probe", origin: self.origin, parentDom, cookie }, "*");</script><p>Sandboxed child</p>`; window.addEventListener("message", (event) => {  if (event.source !== frame.contentWindow) return;  if (!event.data || event.data.lesson !== "sandbox-probe") return;  console.log("event.origin:", event.origin);  console.log("child self.origin:", event.data.origin);  console.log("parent DOM:", event.data.parentDom);  console.log("cookie:", event.data.cookie);}); document.querySelector("#sandbox-output").append(frame);
Sandbox playground: toggle frame powers
Sandbox token modelJavaScript
const tokens = ["allow-scripts"]; function describeSandbox(tokens, sameOriginContent) {  const has = (name) => tokens.includes(name);  return {    scripts: has("allow-scripts") ? "allowed" : "blocked",    forms: has("allow-forms") ? "allowed" : "blocked",    popups: has("allow-popups") ? "allowed" : "blocked",    origin: has("allow-same-origin") ? "real origin" : "opaque origin",    topNavigation: has("allow-top-navigation") ? "allowed" : "blocked",    canRemoveSandbox: sameOriginContent &&      has("allow-scripts") && has("allow-same-origin"),  };} console.log(describeSandbox(tokens, true));
Live iframeallow-scripts
scriptsallowed
formsblocked
popupsblocked
reported originopaque origin
parent DOM readwaiting
cookie accesswaiting
Try it yourself

The real iframe reports from inside only when scripts are allowed. Without allow-same-origin, its origin remains opaque and parent DOM access is blocked.

Chrome may log an intentional sandbox warning if you remove allow-scripts or enable the dangerous same-origin pair. That warning is the browser enforcing the boundary.
Blocked by sandbox or allowed?
  • Running JavaScript in a bare sandbox
  • Submitting a form from the frame
  • Opening a popup from the frame
  • Navigating the top-level page
  • Reading the parent page as same-origin
  • `allow-scripts` plus `allow-same-origin` on same-origin content
Try it yourself
0 of 6 correct

Sort each ability by the sandbox rule it depends on. Start with the assumption that a bare sandbox blocks it.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

srcdoc is convenient for previews because the HTML lives in the attribute instead of a separate URL. A sandboxed srcdoc frame without allow-same-origin receives an opaque origin. The message event exposes that as "null", so source checks become especially important. A response header can apply a similar restriction with the CSP sandbox directive, but an iframe attribute is the usual tool for embeds you create in JavaScript.

This site’s editor case study

The CompleteFrontend editor runs learner code in a sandboxed preview iframe. In both src/components/editor/EditorApp.tsx and InlineEditor.tsx, the sandbox constant grants scripts, forms, popups, pointer lock, downloads, and modals, but not allow-same-origin.

Editor sandbox constantJavaScript
const SANDBOX =  "allow-scripts allow-modals allow-forms allow-popups " +  "allow-popups-to-escape-sandbox allow-pointer-lock allow-downloads"; // CompleteFrontend deliberately does not include allow-same-origin.

The runtime inside public/editor-runtime.js explains why: the preview frame has an opaque origin, so learner code cannot read this site’s sign-in or storage. The runtime then provides in-memory storage, project-file fetches, worker creation, and postMessage-based console reporting.

Editor runtime commentJavaScript
/* * The frame has an opaque origin (no allow-same-origin), so learner code can't reach the * site's storage or sign-in. This file fills in what that origin lacks: localStorage, * sessionStorage, and cookies kept in memory, fetch() of project files, workers made from * project files, and links between project pages. */

Safe postMessage handlers

STEP THROUGH

Isolation still needs communication. postMessage copies data with the structured clone algorithm, so the receiver gets data, not the sender’s live object. Sending code must use a specific targetOrigin for sensitive data. Use "*" only when the target has an opaque origin, such as a sandboxed srcdoc frame, and never for secrets.

Receiving code needs three gates: validate event.origin, validate event.source against the exact window or port you stored, and validate the shape of event.data. The step-through uses a simulated MessageEvent-like object so you can see every accepted and rejected path deterministically.

Step through a safe widget message handler
Step 0 of 7Ready
Your turn: follow the blue line

Choose an incoming message, then step through the origin, source, and shape gates.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const trustedSource = paymentFrame; function isPaymentReady(data) {  return data && data.type === "payment:ready" &&    typeof data.checkoutId === "string";} function onMessage(event) {  if (!allowedOrigins.has(event.origin)) return "reject: origin";  if (event.source !== trustedSource) return "reject: source";  if (!isPaymentReady(event.data)) return "reject: shape";  return "accept: " + event.data.checkoutId;} console.log(onMessage(incomingEvent));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the incoming message
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

For ongoing private conversations, transfer one end of a MessageChannel. The public message event is then just the handshake; the two ports form a private line between the parent and that frame or worker.

postMessage playground: public events and a private port
Private port handshakeJavaScript
const channel = new MessageChannel();channel.port1.onmessage = (event) => {  console.log("private reply", event.data.type);}; trustedWindow.postMessage(  { lesson: "isolation", type: "connect-port" },  "*",  [channel.port2],); channel.port1.postMessage({ type: "private:hello" });
Sandboxed framesorigin null

Use a control to send a message.

Try it yourself

Both frames have the opaque origin null, so the handler also checks the exact event.source. The MessageChannel button transfers a private port after the public handshake.

The demo uses targetOrigin: "*" only because these sandboxed srcdoc frames have opaque origins. It sends no secrets.

Cross-origin isolation

Spectre showed that timing and speculative execution can leak information across boundaries. Browsers responded by restricting SharedArrayBuffer and some high-resolution measurement APIs unless a page opts into stronger process and resource isolation. The runtime check is self.crossOriginIsolated.

Headers that make a page cross-origin isolated
PieceExampleJob
COOPCross-Origin-Opener-Policy: same-originSeparates this browsing context group from cross-origin openers.
COEPCross-Origin-Embedder-Policy: require-corp or credentiallessRequires embedded subresources to opt in or be loaded without credentials.
CORPCross-Origin-Resource-Policy: same-origin or cross-originA resource header that tells isolated pages whether they may embed it.
Runtime checkself.crossOriginIsolatedTrue only when the page and its environment satisfy the isolation rules.
What comes backSharedArrayBuffer and high-precision APIsBrowsers restrict these after Spectre unless cross-origin isolation is active.
Headers, not runnable JavaScriptHTTP
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin
What an isolated page can checkJavaScript
console.log(self.crossOriginIsolated);console.log(typeof SharedArrayBuffer);console.log(typeof performance.measureUserAgentSpecificMemory);

Verified in the installed Chrome 154 for this task: a normal data: page had self.crossOriginIsolated === false, SharedArrayBuffer undefined, and performance.measureUserAgentSpecificMemory undefined. The same browser reported all three available after a routed page sent COOP: same-origin and COEP: require-corp.

What breaks

COEP can break third-party images, scripts, iframes, and analytics that do not send CORS or CORP headers. credentialless helps some cases by loading without credentials, but payment widgets, ads, and social embeds may still need a non-isolated page or a different integration.

Workers, ShadowRealm, and Node vm

A Web Worker is another browser boundary. It runs in a separate global scope and cannot touch the DOM. That makes it useful for parsing, image processing, and data-only plugin work. It is not a full security boundary by itself: a worker can still consume CPU, make allowed network requests, and send bad messages unless the parent validates them.

A worker has no DOMJavaScript
// main pageconst worker = new Worker("plugin-worker.js", { type: "module" });worker.postMessage({ type: "render", text: userText });worker.onmessage = (event) => {  preview.textContent = event.data.text;}; // plugin-worker.jsself.onmessage = (event) => {  // There is no document or DOM in a worker global scope.  self.postMessage({ text: String(event.data.text).toUpperCase() });};

ShadowRealm is a TC39 proposal for creating a fresh ECMAScript realm with its own global object, built-ins, and intrinsics. Its proposed API includes new ShadowRealm(), realm.evaluate(sourceText), and realm.importValue(specifier, bindingName). Checked on 2026-09-26: the TC39 proposal is at Stage 2.7, and Chrome 154 in this environment reports typeof ShadowRealm === "undefined".

Feature-detected ShadowRealm proposal codeJavaScript
if (typeof ShadowRealm === "function") {  const realm = new ShadowRealm();  const twice = await realm.importValue("./plugin.js", "twice");  console.log(twice(21));  console.log(realm.evaluate("Array.prototype === globalThis.Array.prototype"));} else {  console.log("ShadowRealm is not available in this browser yet");}
ShadowRealm is not a complete sandbox

A fresh realm gives isolated built-ins, but it does not automatically solve side channels, infinite loops, memory exhaustion, host APIs you expose, or browser bugs. Treat it as a future language tool, not a replacement for iframe, worker, process, or permission boundaries.

Node’s vm module has a similar naming trap. The official Node.js documentation says: “The node:vm module is not a security mechanism. Do not use it to run untrusted code.” Use OS processes, containers, service isolation, or hardened capability systems when hostile code is in scope.

Node vm warningJavaScript
import vm from "node:vm"; const context = vm.createContext({ answer: 41 });vm.runInContext("answer += 1", context);console.log(context.answer); // Node.js docs: the node:vm module is not a security mechanism.// Do not use it to run untrusted code.

SES and Hardened JavaScript take a capability-oriented approach: freeze or tame the platform, then run code in compartments with only the powers you pass in. That can be a good plugin-system direction, but it is a design commitment, not a line of code you add after a page is already trusting everything.

SES-style compartment sketchJavaScript
import "ses";lockdown(); const compartment = new Compartment({ console });compartment.evaluate("console.log(Array.prototype.map.call([1, 2], x => x * 2))");
Which isolation boundary fits?
BoundaryWhat it isolatesGood fitLimit
Sandboxed iframeRuns a whole document with browser-enforced permissionsUntrusted widgets, ads, HTML previews, and code playgroundsStrongest browser primitive here; still validate messages and tokens
Web WorkerRuns script off the main thread with no DOM accessCPU-heavy parsing, image work, plugin logic that only needs dataNo DOM access, but same-origin network/storage rules and CPU abuse still matter
ShadowRealmFresh ECMAScript realm with separate globals and intrinsicsFuture plugin-style code when availableNot a security boundary for side channels, host powers, or infinite loops
Node vmSeparate V8 context inside one Node processTrusted tooling, tests, or templatesNode docs explicitly say it is not for untrusted code
SES lockdownFreezes and tames intrinsics, then runs code in compartmentsCapability-oriented plugin systemsPromising but requires a disciplined ecosystem and threat model

Real-world patterns

Use sandboxed iframes for ads, comments rendered as HTML, user-created previews, payment widgets, and code playgrounds. Use workers for data-only CPU work. Use cross-origin isolation only on pages that need SharedArrayBuffer, high-precision memory measurement, or libraries that depend on them. Keep XSS defenses from the XSS lesson; sandboxing is defense in depth, not a sanitizer.

  • For a code playground, use sandbox="allow-scripts" plus a message bridge, not same-origin access.
  • For payment widgets, use the provider’s origin, a specific targetOrigin, and a strict message schema.
  • For customer HTML, sanitize first, then preview in a sandbox that does not share your origin.
  • For plugin data processing, start with a worker and a narrow command protocol before considering stronger process isolation.

Common misconceptions

“A sandboxed iframe is automatically safe.”

It depends on tokens and messages. allow-scripts allow-same-origin on same-origin content can defeat the sandbox, and a sloppy message handler can still turn copied data into a bug.

“postMessage with star is always fine because the receiver checks origin.”

The sender still controls delivery. Use a specific targetOrigin for secrets. Use * only for opaque origins and non-secret messages.

“Workers are security sandboxes.”

Workers lack DOM access, which is useful, but they are still code you run. They can loop, allocate memory, and send hostile messages.

“ShadowRealm will make arbitrary plugin code safe.”

It isolates ECMAScript globals. It does not by itself meter CPU, remove side channels, or decide which host powers your app exposes.

Practice exercises

Exercise 1 · Warm-upPredict the opaque origin

Read the token list and predict the origin string a sandboxed frame would report.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const tokens = ["allow-scripts"];
const origin = tokens.includes("allow-same-origin") ? "real origin" : "null";
console.log(origin);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeReject a spoofed source

    Trace the validator and decide whether it accepts or rejects the message.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const trustedOrigin = "https://pay.example";
    const trustedSource = "frame-a";
    const event = { origin: "https://pay.example", source: "frame-b", data: { type: "payment:ready", checkoutId: "co_42" } };
    const shapeOK = event.data.type === "payment:ready" && typeof event.data.checkoutId === "string";
    const ok = event.origin === trustedOrigin && event.source === trustedSource && shapeOK;
    console.log(ok ? "accept" : "reject");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeChoose tokens for a learner code frame

      A code editor needs to run JavaScript examples but must not expose the site’s origin. Which single sandbox token is required?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeName the isolation headers

        Which two response headers are the standard browser route to crossOriginIsolated?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const headers = ["Cross-Origin-Opener-Policy", "Cross-Origin-Embedder-Policy"];
        console.log(headers.join(" + "));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeSpot the sandbox escape review comment

          In review, name the token pair that should not be used as the isolation boundary for same-origin untrusted content.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const tokens = ["allow-scripts", "allow-same-origin"];
          console.log(tokens.includes("allow-scripts") && tokens.includes("allow-same-origin"));

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            Isolation and sandboxing quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What is the plain definition of isolation in this lesson?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this sandbox-origin check print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const tokens = ["allow-scripts"];
              const origin = tokens.includes("allow-same-origin") ? "real origin" : "null";
              console.log(origin);

              Choose an answer to see the explanation.

            3. Question 3 of 7What does the message validator print for a spoofed source?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const trustedOrigin = "https://pay.example";
              const trustedSource = "frame-a";
              const event = { origin: "https://pay.example", source: "frame-b", data: { type: "payment:ready", checkoutId: "co_42" } };
              const shapeOK = event.data.type === "payment:ready" && typeof event.data.checkoutId === "string";
              const ok = event.origin === trustedOrigin && event.source === trustedSource && shapeOK;
              console.log(ok ? "accept" : "reject");

              Choose an answer to see the explanation.

            4. Question 4 of 7When is targetOrigin: "*" acceptable?

              Choose an answer to see the explanation.

            5. Question 5 of 7Which header pair is the usual route to self.crossOriginIsolated === true?

              Choose an answer to see the explanation.

            6. Question 6 of 7What is accurate about ShadowRealm on 2026-09-26?

              Choose an answer to see the explanation.

            7. Question 7 of 7Which statement about Node's vm module matches the official docs?

              Choose an answer to see the explanation.

            Key takeaways

            • Start sandboxed iframes locked down, then add only the tokens the embed actually needs.
            • Never combine allow-scripts and allow-same-origin as a same-origin security boundary.
            • Use postMessage with specific targetOrigin when possible, then validate origin, source, and shape on receipt.
            • Cross-origin isolation is a header opt-in for powerful memory APIs, not a general widget sandbox.
            • Workers, ShadowRealm, Node vm, and SES compartments solve different pieces; choose based on the threat model.

            Isolation lets untrusted code do useful work through a narrow, checked opening instead of handing it the whole page.

            Next in this module, Web Crypto shows how to use browser cryptography APIs without inventing your own primitives. Keep the same habit: choose the browser tool with the boundary you need.

            CompleteFrontend Clear concepts. Working examples.