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
iframeis a separate document; it has its own tree, styles, and scripts, and cannot access the outer document if it comes from a different origin. sandboxshuts off all capabilities the moment it gets written; wanted capabilities get granted back one by one.- When
allow-scriptsandallow-same-originget written together at the same origin, isolation can get removed by the frame itself. sandboxregulates behavior,allowregulates device and platform capabilities; the two do not substitute for each other.titleis 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.