Skip to content
academia.sh

Lesson 18 / 25

Embedded Content

Embedding another document with a frame, the embedded document's isolation, the capability-restriction mechanism, and the performance and accessibility consequences of embedding.

Contents

Audio and video were the document’s own resources. Some content, though, is the whole of another document: a map, a player, a section from another site. Embedding these means placing a second document inside the document.

The iframe element does this job. The element’s name means “inline frame,” and that is exactly what it is: a separate document, loaded on its own and with its own tree, within the document’s flow.

The Embedded Document Is a Separate Document

The document inside iframe is not part of the outer document’s tree. It has its own root, its own head section, its own styles, and its own scripts. The outer document’s style rules do not get applied inside; the script inside cannot access the outer document’s tree directly.

This separation is not a limitation, it is the entire rationale for embedding. Placing content from an uncontrolled source directly into your document gives that content access to the whole document. The frame establishes the boundary between them.

The boundary rests on the concept of origin: the trio of scheme, host, and port. Same-origin documents can access each other’s tree; different-origin ones cannot. The address parts separated in the What the Browser Does lesson in the How the Internet Works curriculum turn into a security boundary here.

The Isolation Attribute

Different-origin separation draws a basic boundary, but is not enough: the embedded document can still run scripts, submit forms, open a new window, navigate the top document to another address. The sandbox attribute shuts these capabilities off.

The attribute works in reverse: the moment it gets written, no capability gets granted; the capabilities wanted back get listed one by one.

// embed.mjs — capability model of the sandbox attribute
const CAPABILITIES = [
  ["allow-scripts", "running scripts"],
  ["allow-forms", "submitting forms"],
  ["allow-popups", "opening a new window"],
  ["allow-same-origin", "being counted as its own origin"],
  ["allow-top-navigation", "navigating the top document elsewhere"],
  ["allow-modals", "opening a modal window"],
];

function resolve(attrValue) {
  if (attrValue === null) return new Set(CAPABILITIES.map(([name]) => name)); // no sandbox
  return new Set(attrValue.split(/\s+/).filter(Boolean));
}

const cases = [
  ["no sandbox attribute", null],
  ["sandbox (empty)", ""],
  ["sandbox=allow-scripts", "allow-scripts"],
  ["sandbox=allow-scripts allow-forms", "allow-scripts allow-forms"],
];

for (const [label, value] of cases) {
  const open = resolve(value);
  console.log("[" + label + "]");
  for (const [key, description] of CAPABILITIES) {
    console.log("  " + (open.has(key) ? "open" : "closed").padEnd(6) + "  " + description);
  }
}

console.log("--- dangerous combination ---");
const both = resolve("allow-scripts allow-same-origin");
console.log(
  "allow-scripts + allow-same-origin:",
  both.has("allow-scripts") && both.has("allow-same-origin")
    ? "the frame can remove its own sandbox attribute"
    : "isolation is preserved",
);
[no sandbox attribute]
  open    running scripts
  open    submitting forms
  open    opening a new window
  open    being counted as its own origin
  open    navigating the top document elsewhere
  open    opening a modal window
[sandbox (empty)]
  closed  running scripts
  closed  submitting forms
  closed  opening a new window
  closed  being counted as its own origin
  closed  navigating the top document elsewhere
  closed  opening a modal window
[sandbox=allow-scripts]
  open    running scripts
  closed  submitting forms
  closed  opening a new window
  closed  being counted as its own origin
  closed  navigating the top document elsewhere
  closed  opening a modal window
[sandbox=allow-scripts allow-forms]
  open    running scripts
  open    submitting forms
  closed  opening a new window
  closed  being counted as its own origin
  closed  navigating the top document elsewhere
  closed  opening a modal window
--- dangerous combination ---
allow-scripts + allow-same-origin: the frame can remove its own sandbox attribute

The first two cases show that whether the attribute gets written makes the entire difference. An empty sandbox also places the embedded document into a separate origin: since it does not count as the same origin as its own server, it cannot access stored data or cookies.

The last line declares two values that should not get written together. allow-same-origin counts the embedded document as the same as its own origin; allow-scripts permits running scripts. With both together and a shared origin, the script inside the frame can remove the outer document’s sandbox attribute and reload the frame — isolation cancels itself out.

Capability Permissions

sandbox restricts the document’s behavior; the allow attribute, though, regulates access to device and platform capabilities: camera, microphone, location, full screen.

<iframe src="https://map.test/north-slope"
        title="The station's location on the map"
        width="640" height="480" loading="lazy"
        sandbox="allow-scripts"
        allow="fullscreen; geolocation 'none'"
        referrerpolicy="no-referrer"></iframe>

The default is that access is closed for most capabilities; the allow declaration opens it, and the 'none' value closes it explicitly. These two attributes do not substitute for each other and get used together.

The referrerpolicy attribute declares how much information about the requesting document gets sent to the embedded resource. The no-referrer value sends none; the embedded third party cannot learn which page the user is on.

Name and Loading

The title attribute gives the frame an accessible name. If it does not get written, a screen reader announces the frame as an unnamed region, and the user cannot tell what it is without entering it. This attribute is not optional on embedded content.

loading="lazy" declares that the frame does not get loaded until it nears the viewport. Because the embedded document also requests its own subresources, its cost is much higher than a single image’s; for a map or player at the bottom of the page, this declaration noticeably lowers opening cost.

The width and height attributes do the space-allotting job here too; a frame with no size declaration shifts the content below it once it loads.

Protecting Against Being Embedded

Embedding runs both ways: just as a document can embed another, others can embed this one in their own documents too. This can get used to mislead the user about which site they are interacting with — the visible document is yours, the transparent layer placed on top of it is someone else’s, and the place the user clicks and the button they think they pressed pull apart.

Protection gets declared not in the document but in the server response. The frame-ancestors directive in the Content-Security-Policy header lists which origins can embed the document.

Content-Security-Policy: frame-ancestors 'self'

This declaration allows the document to get embedded only by pages on its own origin; the 'none' value permits no embedding at all. As in the Meta Tags lesson, this can also get written with http-equiv, but not for this directive: the embedding decision gets made before the document gets parsed, so the declaration has to exist in the response header.

This header gets written on every document that carries session data or initiates a transaction. If it does not get written, embedding the document is free, letting a page outside the document’s control put it before the user in any context.

The Cost of Embedding

Every embedded document starts an independent loading chain: its own name resolution, its own connection, its own subresources. All the steps listed in the Resource Loading Order lesson get carried out again, and these steps share the same bandwidth as the outer document’s resources.

Where a function works without embedding, not embedding gets preferred. If an interactive map is not needed to show a location, an image and a coordinate text give the same information much more cheaply. The station document’s choice runs the same way: the location got told with a figure, and the map did not get embedded.

Summary

  • The document inside iframe is a separate document; it has its own tree, styles, and scripts, and cannot access the outer document if it comes from a different origin.
  • sandbox shuts off all capabilities the moment it gets written; wanted capabilities get granted back one by one.
  • When allow-scripts and allow-same-origin get written together at the same origin, isolation can get removed by the frame itself.
  • sandbox regulates behavior, allow regulates device and platform capabilities; the two do not substitute for each other.
  • title is required on embedded content; loading="lazy" and a size declaration limit the embedding’s opening cost and content shift.

Next Step

This topic completed the elements that give the document meaning. What remains is a mechanism with no defined meaning of its own, but useful for attaching data to the document: attributes that can get attached to any element, produce no name collision, and can get read by scripts. The next lesson takes up this mechanism and how an attribute’s name gets translated into its program-side counterpart.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close