Isolation & sandboxing
Run untrusted JavaScript, widgets, ads, and user content safely with iframe sandboxing, postMessage checks, COOP and COEP.
- 01Lock down a framed pagePick only the sandbox tokens an
iframeneeds, and never rely onallow-scriptsplusallow-same-origin. - 02Check messages from a frameSend with a real
targetOrigin, then check the origin, the source, and the shape of every message. Use aMessageChannelfor a private line. - 03Pick the right toolSay what iframes, workers, cross-origin isolation, ShadowRealm, Node
vm, and SES each keep apart, and what they don't.
Keep untrusted code apart
Many web pages run code or show content that their own team did not write. Here are some common examples:
- analytics scripts and ads from other companies
- payment widgets, like a card form from a payment company
- HTML that customers wrote, shown in a preview
- plugins that other people write for your app
- code that learners type into a playground, like the editor on this site
We call this code untrusted: you did not write it, so you can't be sure what it will do. It may have a bug, or it may try to do harm. So don't ask "do we trust it?" Ask "what stops it from reaching the rest of the page?"
Isolation means running untrusted code or content in its own separate space. That space gives it only the powers it needs. It talks to your page only through a small channel, and your page checks every message that comes through.
A sandbox is one way to build that separate space. The word comes from a children's sandbox: play happens inside it, and the sand stays inside it.
This lesson builds on windows and frames, messaging and structured cloning, eval, and XSS. You'll learn these tools, in this order:
- origins, the browser's basic rule for what pages may read (a short review of the CORS lesson)
- the
sandboxattribute on aniframe - safe checks for messages sent with
postMessage - the COOP and COEP headers, which turn on cross-origin isolation
- workers, ShadowRealm, and Node's
vmmodule, and what each one can and can't keep apart
You hire a guest to do a job in your house. You don't give them every key. You put them in one room with the tools they need, and lock the door. They pass notes to you through a small hatch. You read each note before you do what it says.
- In real life: A separate locked room
- In JavaScript: A sandboxed iframe, a worker, or a separate process
- In real life: A small hatch in the door for notes
- In JavaScript:
postMessageor aMessagePort - In real life: Rules about what the guest may use
- In JavaScript: Sandbox tokens, like
allow-scripts - In real life: Reading each note before you act on it
- In JavaScript: Checking each message before you use it
Origins and boundaries
The browser already has one basic rule that keeps pages apart. It is called the same-origin policy. An origin is made of three parts of a URL:
- the scheme, like
https - the host, like
shop.example - the port, like
8443(forhttps, the port is443when the URL doesn't show one)
Two pages are same-origin only when all three parts match. The path, like /cart, is not part of the origin.
const page = new URL("https://shop.example/cart");
const help = new URL("https://shop.example/help");
const otherPort = new URL("https://shop.example:8443/cart");
console.log(page.origin === help.origin);
console.log(page.origin === otherPort.origin);Line 1 makes a URL for the cart page. Line 2 keeps the same scheme, host, and port, but changes the path to /help. So line 4 prints true. Line 3 changes the port to 8443. So line 5 prints false.
What does the rule allow? Pages from the same origin can usually read each other's page content (the DOM) and storage, like localStorage. Cookies have their own extra rules: their domain and path, and the HttpOnly flag, which hides a cookie from JavaScript. Pages from different origins usually can't read each other's DOM or storage.
The CORS lesson was about reading data over the network. This lesson asks a different question: what can a frame or a script do once it is already on your page?
Code from other companies still runs in your user's browser. That is why it needs a boundary. Here is what each kind of code should not be able to do:
- A map widget should not read your CSRF token, the secret your forms send to prove a request is real.
- A preview of customer HTML should not reach your admin pages.
- A plugin should not freeze the page.
- A code editor, like the one on this site, should run examples without giving them the completefrontend.com origin.
Each tool in this lesson keeps a different thing apart:
- A sandboxed iframe keeps a whole page apart from yours.
- A worker keeps code away from the DOM and off the main thread.
- COOP and COEP keep your page apart from other sites, so that powerful memory tools can be turned on.
- Message checks make sure only the right messages change your page.
None of them replaces safe HTML rendering, careful token storage, or the defenses in CSRF and tokens.
The iframe sandbox
BROWSER LABAn iframe shows one web page inside another. Add the sandbox attribute to it, with no value, and the browser takes powers away from the framed page. A bare sandbox like that blocks all of these:
- running scripts
- sending forms
- opening popups
- sending the whole browser tab to a new address (called top navigation)
- pointer lock, downloads, and dialogs like
alert() - the page's normal origin
That last one matters most. Instead of its real origin, the framed page gets an opaque origin: a special origin that matches no other origin. Message events show it as the text "null".
Then you give back only the powers the framed page needs. Each power has its own word, called a token. You list the tokens in the sandbox attribute. Common tokens are allow-scripts, allow-forms, allow-popups, and allow-same-origin.
const tokens = ["allow-scripts"];
console.log(tokens.includes("allow-scripts"));
console.log(tokens.includes("allow-forms"));Line 1 lists one token, allow-scripts. Line 2 asks if that token is in the list, and prints true. Line 3 asks about allow-forms, and prints false. So the frame can run JavaScript, but it still can't send forms. Add allow-forms only if the frame really needs to send a form.
sandbox blocks everything. The allow-scripts token gives back one power: running JavaScript. Forms stay blocked until you add allow-forms too.The allow-same-origin token needs care. It lets the framed page keep its own real origin instead of the opaque one. It does not give a page from another site your origin.
The danger comes when the framed page is from your own origin. Then allow-scripts plus allow-same-origin lets its scripts reach the iframe element in your page. They can remove the sandbox attribute and reload without it. To stay safe, do one of these:
- serve untrusted content from a different origin, or
- leave out one of the two tokens.
Never rely on sandbox="allow-scripts allow-same-origin" to protect your page from untrusted content that comes from your own origin.
allow-scripts, the frame's origin is "null" and the parent page is out of reach. Add allow-same-origin to content from your own origin, and the frame can take its own sandbox off.Popups and top navigation also need their own tokens. A popup opened from a sandboxed frame gets the same sandbox rules. The allow-popups-to-escape-sandbox token lets a popup open without them.
In the step-through below, pick a set of tokens. Then step through and watch which powers each token gives back.
Pick a list of tokens. Then step through to see which powers stay blocked.
script
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));The next snippet makes a real sandboxed iframe inside the lesson editor's page. It allows scripts, so the framed page can report back. It never allows allow-same-origin. Here is what happens, line by line:
- Lines 1 to 3 make the
iframe, give it onlyallow-scripts, and put a small page inside it withsrcdoc. - Line 5 tries to read the parent page. The browser stops it with a
SecurityError. - Lines 6 to 8 try to set a cookie. That is blocked too, because an opaque origin has no cookies.
- Line 9 sends a report to the parent page with
postMessage. - Line 13 ignores any message that doesn't come from this frame.
- Lines 15 to 18 print the report. Both origins print as
null.
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);In the playground, turn tokens on and off. A real iframe reports what it can do each time.
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));opaque originwaitingwaitingThe real iframe can report back only when scripts are allowed. Without allow-same-origin, its origin stays opaque, and reading the parent page is blocked.
allow-scripts or turn on the unsafe pair. That warning is the browser enforcing the sandbox.- Running JavaScript in the frame
- Sending a form from the frame
- Opening a popup from the frame
- Sending the whole tab to a new address
- Reading the parent page's DOM
`allow-scripts` plus `allow-same-origin` on content from your own origin
A widget frame has sandbox="allow-scripts allow-forms allow-popups". Sort each power: allowed by a token in that list, or still blocked? One card is the unsafe pair.
The srcdoc attribute is handy for previews. It holds the HTML of the framed page right in the attribute, so you don't need a separate URL. A sandboxed srcdoc frame without allow-same-origin gets an opaque origin. Its messages show the origin as "null". Many frames can share that same "null", so you must also check which frame sent a message.
A server can apply the same rules to a whole page with the CSP sandbox header. For a frame you make in JavaScript, the sandbox attribute is the usual tool.
How this site's editor uses a sandbox
The CompleteFrontend editor runs learner code in a sandboxed preview iframe. Two files set it up: src/components/editor/EditorApp.tsx and InlineEditor.tsx. Both use the same list of tokens. It allows scripts, forms, popups, pointer lock, downloads, and dialogs. It does not allow allow-same-origin.
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.A comment in public/editor-runtime.js explains why. The preview frame has an opaque origin, so learner code can't reach this site's storage or sign-in. That file then adds back safe stand-ins for what the frame lost:
localStorage,sessionStorage, and cookies that live only in memoryfetch()for files in your project- workers made from files in your project
- console output, sent to the editor with
postMessage
/* * 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 message handlers
STEP THROUGHAn isolated frame still needs to talk to your page. It uses postMessage, which sends a copy of the data. The copy is made with the structured clone algorithm, the browser's way of deep-copying data. So the receiver gets plain data, never the sender's live object.
When you send, the second argument is the targetOrigin. It names the origin that is allowed to receive the message. Use a real origin, like "https://pay.example", for anything private. Use "*", which means "any origin", only when the receiver has an opaque origin you can't name. A sandboxed srcdoc frame is one example. Never send secrets with "*".
When you receive, check three things before you use the message:
- Origin: is
event.originon your list of allowed origins? - Source: is
event.sourcethe exact frame window you saved when you made the frame? - Shape: does
event.datahave the type and fields you expect?
const paymentFrame = {};
const ready = { origin: "https://pay.example", source: paymentFrame, data: { type: "payment:ready", checkoutId: "co_42" } };
const lookalike = { ...ready, source: {} };
const accepts = (event) => event.origin === "https://pay.example" && event.source === paymentFrame &&
event.data && !Array.isArray(event.data) && event.data.type === "payment:ready" &&
typeof event.data.checkoutId === "string";
const cases = [ready, lookalike, { ...ready, data: [] }];
console.log(cases.map((event) => accepts(event) ? "accept" : "reject").join(", "));This code uses plain objects that look like message events, so you can run it anywhere. Line by line:
- Line 1 stands in for the payment frame you made.
- Line 2 is a good message from that frame.
- Line 3 copies it, but changes the source to a different frame. Same origin, wrong frame.
- Lines 4 to 6 check the exact origin and the exact frame. Then they check that the data is an object (not an array), its type is
"payment:ready", and its checkout ID is a string. - Line 7 adds a third message whose data is an empty array.
- Line 8 prints
accept, reject, reject.
The second message has the right origin, but it is still rejected. A matching origin alone is not enough.
Two more details about origins:
- When you send,
targetOriginalso protects you if the frame has moved to another site. The browser won't deliver the message there. - When you receive,
event.originis the sender's origin at the moment it sent the message. It does not change if that window moves later.
In the step-through below, pick an incoming message. Then watch which check stops it.
Pick an incoming message. Then step through the three checks: origin, source, and shape.
script
const trustedSource = globalThis.paymentFrame ?? {};const incomingEvent = globalThis.incomingEvent ?? { origin: "https://pay.example", source: trustedSource, data: { type: "payment:ready", checkoutId: "co_42" } };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));For a longer private conversation, use a MessageChannel. It makes two connected ends, called ports. You keep one port and send the other to the frame or worker, inside your first postMessage. After that, the two ports talk only to each other, like a private phone line. Other frames can't send to it.
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" });Press a button to send a message.
Both frames have the origin null, so the origin check can't tell them apart. The handler also checks the exact event.source. The last button sends one end of a MessageChannel, a private line, to the trusted frame.
targetOrigin: "*" only because these sandboxed srcdoc frames have opaque origins. It sends no secrets.Cross-origin isolation
In 2018, researchers found a kind of attack called Spectre. It measures tiny differences in how long the processor takes to do things. From those timings, a page can sometimes guess data from memory it should never see, even data from another site.
Browsers responded by turning off SharedArrayBuffer (memory that several threads can share) and some very precise timing and memory tools. A page gets them back only if it asks to be kept strictly apart from other sites. This is called cross-origin isolation. Your code can check it with self.crossOriginIsolated, which is true or false.
The table below lists the pieces. Read each row as: this piece, what it looks like, and what it does.
| Piece | What it looks like | What it does |
|---|---|---|
| COOP | Cross-Origin-Opener-Policy: same-origin | A header on your page. It cuts the link to windows from other sites. |
| COEP | Cross-Origin-Embedder-Policy: require-corp or credentialless | A header on your page. Every file it loads must agree to be loaded, or is loaded without cookies. |
| CORP | Cross-Origin-Resource-Policy: same-origin or cross-origin | A header on each loaded file. It says which sites may load that file. |
| The check in code | self.crossOriginIsolated | It is true only when the page meets all the isolation rules. |
| What you get back | SharedArrayBuffer and precise timing and memory tools | Browsers turned these off after Spectre. They come back only on isolated pages. |
| Permissions Policy | cross-origin-isolated | This setting can still block those tools, even when the headers are there. |
Three of these pieces are headers, but they go on different responses:
- Cross-Origin-Opener-Policy (COOP) is a header on your page. It cuts the link between your page and windows from other sites that opened it, or that it opens.
- Cross-Origin-Embedder-Policy (COEP) is also a header on your page. It says that every image, script, or frame your page loads must agree to be loaded.
- Cross-Origin-Resource-Policy (CORP) is a header on each of those loaded files. It is how a file agrees. A file can also agree through CORS.
One more rule can still block the powerful tools: the cross-origin-isolated setting of Permissions Policy. So always check self.crossOriginIsolated in code. Don't assume the headers worked.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corpconsole.log(self.crossOriginIsolated);console.log(typeof SharedArrayBuffer);console.log(typeof performance.measureUserAgentSpecificMemory);Line 1 prints whether the page is cross-origin isolated. Line 2 prints the type of SharedArrayBuffer. Line 3 prints the type of a memory tool, performance.measureUserAgentSpecificMemory. On a normal page, lines 2 and 3 print undefined.
const useSharedMemory =
self.crossOriginIsolated && typeof SharedArrayBuffer === "function";
const buffer = useSharedMemory ? new SharedArrayBuffer(16) : new ArrayBuffer(16);
console.log(buffer.constructor.name);Lines 1 and 2 check two things: is the page isolated, and does SharedArrayBuffer exist? Line 3 makes shared memory only when both are true. Otherwise it makes an ordinary ArrayBuffer. Line 4 prints ArrayBuffer in the lesson editor, because its preview is not cross-origin isolated.
self.crossOriginIsolated is false and the code falls back to an ordinary ArrayBuffer. With COOP and COEP, it is true and shared memory is allowed.We checked this in Chrome 154 while writing this lesson. On a normal data: page, self.crossOriginIsolated was false, and both SharedArrayBuffer and performance.measureUserAgentSpecificMemory were undefined. On a page sent with COOP: same-origin and COEP: require-corp, all three were available.
COEP blocks images, scripts, frames, and analytics from other sites that don't send CORS or CORP headers. The credentialless value helps in some cases: it loads those files without cookies. But payment widgets, ads, and social media embeds may still break. Then you need a page without isolation, or a different way to add them.
COOP can also cut the link to a popup. Some sign-in flows use a popup that talks back to your page, so test them before you turn these headers on.
Workers, ShadowRealm, and Node vm
A Web Worker runs a script in the background, on its own thread. It has its own set of global variables, and it can't touch the DOM. That makes it a good place for work that only needs data, like these:
- parsing a big file
- processing an image
- plugin code that turns data into other data
But a worker alone does not keep harmful code safe. It can still keep the processor busy, use lots of memory, make network requests that its origin allows, and send bad messages. Your page must still check what comes back.
// 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() });};Line 2 starts the worker. Line 3 sends it some text. Line 4 sets up a function for the worker's reply, and line 5 shows the reply as plain text. Inside the worker, line 9 receives the message. Line 10 is a reminder that there is no document here. Line 11 sends back the text in capital letters.
ShadowRealm is a proposal, an idea for a new JavaScript feature that is not finished yet. TC39, the group that designs JavaScript, is working on it. A realm is a complete set of JavaScript's built-in objects, like Array and Object, plus its own global object. A ShadowRealm would give you a fresh one. The proposed tools are:
new ShadowRealm()to make a new realmrealm.evaluate(sourceText)to run a string of code inside itrealm.importValue(specifier, bindingName)to load one exported value from a module inside it
The proposal is at Stage 2.7, which means it is close to done but not final. In Chrome 154, typeof ShadowRealm is "undefined". So the example below checks first, and the lesson does not run it.
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");}Line 1 checks that ShadowRealm exists. Line 2 makes a realm. Line 3 loads a function called twice from a module inside it, and line 4 calls it. Line 5 runs a string of code inside the realm with evaluate. Inside the realm, Array.prototype and globalThis.Array.prototype are the same object, so it would print true. In today's Chrome, only line 7 runs.
A fresh realm gives code its own built-in objects. It does not stop code that loops forever or uses up all the memory. It does not block timing attacks, and it does not make safe any tool you hand to the code. Think of it as a future language feature. It does not replace an iframe, a worker, a separate process, or careful permissions.
Node's vm module has a similar trap in its name. It runs code in a separate set of global variables. The official Node.js documentation says: "The node:vm module is not a security mechanism. Do not use it to run untrusted code." When code may be harmful, use a stronger boundary:
- a separate operating-system process
- a container
- a separate service on another machine
- a carefully built system that hands code only the tools it needs
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.Line 3 makes a context, a separate set of global variables, with answer set to 41. Line 4 runs code inside it that adds 1. Line 5 prints 42. Lines 7 and 8 repeat the warning from the Node docs.
SES (Secure ECMAScript), also called Hardened JavaScript, takes another path. It works in two steps:
lockdown()freezes JavaScript's built-in objects, so code can't change them.- A
Compartmentruns code with only the tools you pass in. Nothing else is reachable.
This can be a good base for a plugin system. But your whole app must be built around it from the start. You can't add it in one line to a page that already trusts every script.
import "ses";lockdown(); const compartment = new Compartment({ console });compartment.evaluate("console.log(Array.prototype.map.call([1, 2], x => x * 2))");Line 1 loads SES. Line 2 freezes the built-in objects. Line 4 makes a compartment and passes in only console. Line 5 runs code inside it, which prints the array [2, 4].
The table below compares all five tools. Read each row as: this tool, what it keeps apart, when to use it, and what it does not do.
| Tool | What it keeps apart | When to use it | What it does not do |
|---|---|---|---|
| Sandboxed iframe | A whole page, with only the powers its tokens allow. | Widgets, ads, previews of customer HTML, and code playgrounds. | It does not check messages or tokens for you. You still have to. |
| Web Worker | Code that runs off the main thread and can't touch the DOM. | Heavy data work, image work, and plugin code that only needs data. | It can still keep the processor busy and use the network and storage its origin allows. |
| ShadowRealm | A fresh set of global variables and built-in objects, like Array. | Plugin-style code in the future, once browsers support it. | It does not stop endless loops, timing attacks, or misuse of tools you hand in. |
Node vm | A separate set of global variables inside one Node process. | Trusted tools, tests, or templates. | The Node docs say it is not for untrusted code. |
| SES lockdown | Frozen built-in objects, plus compartments that get only the tools you pass in. | Plugin systems built around passing in only chosen tools. | Your whole app must be built around it from the start. |
Choosing a boundary in real apps
Pick the tool by what the code needs:
- It needs to show something on screen, like ads, comments written in HTML, previews, payment widgets, or code playgrounds: use a sandboxed iframe.
- It only needs to crunch data, with no DOM: use a worker.
- It needs
SharedArrayBufferor precise memory measurement: turn on cross-origin isolation, on that page only. - It may be actively harmful: run it in a separate operating-system process or container, with limits on processor time and memory. Node's
vmalone is not enough.
Keep the defenses from the XSS lesson too. A sandbox is an extra layer of defense. It does not clean dangerous HTML for you.
Here is what that looks like for four common jobs:
- A code playground: use
sandbox="allow-scripts", and send results back with messages. Don't give the frame your origin. - A payment widget: load it from the payment company's origin. Send to that exact
targetOrigin, and check every message against a strict list of allowed shapes. - Customer HTML: clean it first. Then preview it in a sandbox that doesn't share your origin.
- Plugin data work: start with a worker and a small set of allowed commands. Move to a separate process if the risk is higher.
Common mix-ups
"A sandboxed iframe is always safe."
It depends on the tokens and on your message handler. allow-scripts plus allow-same-origin on content from your own origin can undo the sandbox. And a careless message handler can still let a bad message change your page.
"Sending with "*" is fine, because the receiver checks the origin."
The receiver's check can't stop a message from reaching the wrong window. Only the sender's targetOrigin can. Use a real origin for anything secret. Use "*" only for opaque origins and messages with no secrets.
"Workers keep harmful code safe."
Workers can't touch the DOM, which helps. But they still run code. That code can loop forever, use up memory, and send harmful messages.
"ShadowRealm will make any plugin code safe."
It gives code its own built-in objects and global variables. It does not limit processor time, block timing attacks, or decide which tools your app hands to the code.
Practice exercises
Read the list of tokens. What origin would this sandboxed frame report?
const tokens = ["allow-scripts"];
const origin = tokens.includes("allow-same-origin") ? "real origin" : "null";
console.log(origin);The list has allow-scripts but not allow-same-origin. So the frame gets an opaque origin, and the code prints null.
Follow the checks. Does the code accept or reject the message?
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");The message comes from frame-b, but the saved frame is frame-a. The source check fails, so the code prints reject.
A code editor must run JavaScript examples, but must not give them the site's origin. Which one sandbox token does it need?
Use allow-scripts so JavaScript can run. Leave out allow-same-origin, so the frame keeps an opaque origin.
Which two response headers does a page send to make crossOriginIsolated true?
const headers = ["Cross-Origin-Opener-Policy", "Cross-Origin-Embedder-Policy"];
console.log(headers.join(" + "));Send Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy together. A common pair is COOP: same-origin plus COEP: require-corp.
You are reviewing a teammate's code. Which two tokens should never be used together to protect your page from untrusted content from your own origin?
const tokens = ["allow-scripts", "allow-same-origin"];
console.log(tokens.includes("allow-scripts") && tokens.includes("allow-same-origin"));Flag allow-scripts plus allow-same-origin on untrusted content from your own origin. Together they let the framed page remove its own sandbox.
Check your understanding
Question 1 of 7What does isolation mean in this lesson?
Choose an answer to see the explanation.
Question 2 of 7What does this sandbox-origin check print?
Read the code, then predictconst tokens = ["allow-scripts"]; const origin = tokens.includes("allow-same-origin") ? "real origin" : "null"; console.log(origin);Choose an answer to see the explanation.
Question 3 of 7What does this message check print for a message from the wrong frame?
Read the code, then predictconst 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.
Question 4 of 7When is it OK to send with
targetOriginset to"*"?Choose an answer to see the explanation.
Question 5 of 7Which two headers usually make
self.crossOriginIsolated === true?Choose an answer to see the explanation.
Question 6 of 7What is true about ShadowRealm?
Choose an answer to see the explanation.
Question 7 of 7Which sentence about Node's
vmmodule matches the official docs?Choose an answer to see the explanation.
Key takeaways
- Start a sandboxed iframe with every power blocked. Then add back only the tokens it really needs.
- Never use
allow-scriptsplusallow-same-originto protect your page from content from your own origin. - Send with a real
targetOriginwhen you can. When you receive, check the origin, the source, and the shape. - Cross-origin isolation is turned on with headers, for pages that need powerful memory tools. It is not a sandbox for widgets.
- Workers, ShadowRealm, Node
vm, and SES each keep a different thing apart. Choose by what could go wrong.
Isolation lets untrusted code do useful work through a small, checked channel, instead of giving it your whole page.
Next in this module, Web Crypto shows the browser's built-in tools for secure randomness, hashing, and encryption, so you never write your own. The habit is the same: pick the browser tool that gives you the protection you need.