To host untrusted HTML safely, keep it off the application origin, run it in a sandboxed iframe with the fewest capabilities required, mediate every stored-object request through current access policy, validate uploads and archives before storage, and maintain reporting, quarantine, expiry, and deletion controls. No single layer makes arbitrary HTML risk-free.
Download the untrusted HTML hosting security review
to evaluate an implementation or vendor. It turns the model below into 34 evidence-bearing
PASS / FAIL / N/A checks; “we use a sandbox” is not enough evidence to pass.
Define the untrusted HTML threat model
Untrusted HTML is markup, CSS, JavaScript, media, or archive content whose publisher is not allowed to inherit the privileges of the hosting application. It may come from an unknown uploader, a customer, a coding agent, a compromised account, or a well-intentioned author who accidentally bundled a secret or unsafe dependency.
The word untrusted describes the boundary, not the person's motive. A useful model assumes the artifact may try to:
- read application cookies, browser storage, DOM content, or account data;
- navigate or visually imitate a login, billing, or consent surface;
- send form data or network requests to an external recipient;
- open popups, trigger downloads, register persistence, or abuse browser permissions;
- reach another customer's content through a shared origin or guessed storage path;
- exhaust CPU, memory, storage, decompression, or request capacity;
- remain available after deletion, expiry, visibility change, version replacement, or quarantine.
The browser's same-origin policy restricts how documents from different scheme, host, and port tuples interact. It is a critical isolation primitive, but it is not a complete hosting policy: cross-origin pages can still make requests, navigate, communicate through approved channels, mislead viewers, or consume resources.
Map threats to controls and residual risk
| Threat | Primary control | Evidence to request | Residual risk |
|---|---|---|---|
| Read account cookies or DOM | Separate origin plus sandbox without allow-same-origin | Exact serving origins and rendered iframe attributes | Browser or configuration defects; sensitive data deliberately passed into the frame |
| Replace or imitate trusted UI | No top navigation; clear reader chrome; separate origin | Sandbox tokens, visible URL, phishing-report path | In-frame social engineering remains possible |
| Read another upload's storage | Opaque or per-artifact origin; private object access | Storage policy and cross-artifact browser test | Shared remote APIs or overly broad CORS can reconnect artifacts |
| Serve content after policy change | Policy gateway or short-lived authorization | Delete/expire/quarantine/version test | Caches, stale replicas, or fail-open dependencies |
| ZIP traversal or decompression abuse | Path normalization, file-count and expanded-size limits | Adversarial archive tests before extraction | Parser vulnerabilities and resource exhaustion below the ceiling |
| Malicious links or scripts | Script/network policy, scanning, reports, quarantine | Block/flag rules, asynchronous checks, operator response | Novel domains, first-seen abuse, false negatives, false positives |
| Secret disclosure by publisher | Preflight, warnings, narrow access policy | Artifact review and clean-session visibility test | The host cannot make an embedded reusable credential safe |
The control column is deliberately plural. If the design depends on one perfect scanner, one secret URL, or one iframe token, a bypass in that layer becomes a full compromise.
Choose sanitization or isolation
“Render HTML” can mean two different jobs, and they require different answers.
| User need | Correct model | What changes |
|---|---|---|
| Show comments, descriptions, or rich text inside the trusted application | Sanitize to an explicit allowlist, encode for the output context, and keep scripts disabled | Dangerous elements, attributes, URLs, and behavior are removed or rewritten |
| Preserve a complete HTML presentation, prototype, report, or agent artifact | Isolate the document in a separate browsing context and origin | The artifact remains executable, but loses application privileges |
| Display plain text or code | Use a text sink such as textContent | Markup is displayed rather than interpreted |
The OWASP XSS Prevention Cheat Sheet recommends safe sinks and HTML sanitization where applications intentionally render untrusted HTML inside their own DOM. It also describes Content Security Policy as defense in depth, not the primary fix for an injection flaw.
Sanitization is not a transparent way to host an arbitrary interactive document. Removing scripts, event handlers, forms, embeds, or URLs can break the artifact and produce a file different from the one its author reviewed. Conversely, placing a sanitized comment system in a powerful iframe is usually unnecessary complexity. Decide whether behavior must survive before choosing the control.
Separate trusted and untrusted origins
Do not serve uploaded executable HTML from the same origin as login, account, billing, admin, API,
or other customer content. A path such as app.example.com/uploads/a.html is still same-origin with
app.example.com/account; paths are not security boundaries.
A safer shape is:
app.example.com trusted UI, sessions, writes, account APIs
content.example.net reader shell and/or mediated artifact delivery
private object store no unauthenticated public bucket path
Using a separate registrable domain can further reduce the consequences of cookie scope and future subdomain mistakes. Whatever naming scheme you choose, document which origin owns sessions and which origin is allowed to execute uploaded code.
MDN's iframe reference
warns that sandboxing is ineffective if an attacker can open the same content outside the sandbox
and recommends serving potentially hostile content from a separate origin. It also warns against
combining allow-scripts and allow-same-origin for same-origin embedded content, because the
embedded document can remove the sandbox attribute.
Keep the content origin low privilege
- Do not set application session cookies for the content origin.
- Do not expose account, billing, administrative, or write APIs there.
- Do not let an internal host override or preview header work in production.
- Treat every CORS response as an explicit data-release decision, not a substitute for authorization.
- Avoid a raw-file URL that viewers can open without the intended isolation or access check.
- Test custom domains, embed mode, previews, downloads, and “open in new tab” as separate paths.
Origin separation limits blast radius; it does not prevent phishing inside the artifact, outbound network requests, excessive computation, or sensitive content supplied by the publisher.
Sandbox the runtime with the fewest capabilities
An iframe sandbox begins with restrictions and selectively restores capabilities through tokens.
MDN documents that omitting allow-same-origin gives the framed resource a special origin that
fails same-origin checks, while omitting allow-top-navigation prevents it from replacing the
top-level reader page.
Start with an empty sandbox and add only a capability the artifact actually needs:
<iframe sandbox="allow-scripts" title="User-supplied document"></iframe>
Interactive presentations usually need JavaScript. Forms, popups, downloads, modals, clipboard, fullscreen, orientation, or pointer lock are separate decisions. Record why each restored token or Permissions Policy feature is required, what abuse it enables, and how it was tested.
| Capability | Benefit | Security or product cost | Review question |
|---|---|---|---|
allow-scripts | Runs transitions, charts, and controls | Enables arbitrary computation and network-capable code | Can the artifact function without it? |
allow-forms | Submits interactive forms | Can send viewer-entered data to another endpoint | Is destination and consent visible? |
allow-popups | Opens external links in a new context | Enables popup abuse and opener/referrer concerns | Are links user-initiated and isolated? |
allow-popups-to-escape-sandbox | Lets the destination behave normally | The new page no longer inherits frame restrictions | Is the destination clearly outside the hosted artifact? |
allow-same-origin | Restores normal origin behavior and storage | Can reconnect the frame to shared-origin data and weaken same-origin sandboxing | Is a unique origin guaranteed per artifact? |
allow-top-navigation | Lets the frame replace the reader | Enables forced navigation and phishing flows | Why should uploaded content control the viewer tab? |
The HTTP Content-Security-Policy: sandbox
directive can apply similar restrictions to a resource response. CSP also helps constrain scripts,
frames, connections, images, fonts, forms, and framing, but a policy must match the actual delivery
architecture. A broad connect-src, wildcard CORS rule, or unsandboxed raw path can undo the intent
of a narrow reader shell.
Treat message channels as untrusted input
If parent and frame communicate with postMessage, keep the protocol small. The parent should
validate message type, shape, size, and—where the frame has a stable origin—the exact origin and
window reference. Never evaluate a message as code or insert it into a trusted DOM with an unsafe
sink. An opaque-origin frame may serialize its origin as null, so avoid sending secrets and bind
the exchange to the specific window reference and a minimal command set.
Mediate storage and every content-delivery path
Sandboxing the visible reader is insufficient if a predictable object URL exposes the same HTML without policy or if a public bucket keeps old versions reachable. Storage and delivery need their own authorization boundary.
The OWASP File Upload Cheat Sheet
recommends allowlisting required extensions, validating rather than trusting the request
Content-Type, limiting filenames and size, storing files on a separate server or outside the web
root, and mapping public access through an application handler.
A content request should answer all of these questions before bytes leave storage:
- Does the artifact still exist and remain active?
- Is this the current version or an explicitly authorized historical version?
- Has it expired, been deleted, or entered quarantine?
- Does its visibility allow this unauthenticated path?
- Does a password, allowlist, or private policy require a separate authorization flow?
- If the policy service is unavailable, does delivery fail closed?
Public and unlisted content can use a gateway that checks current state on every request. Protected
content can use a short-lived signed URL after viewer authorization. Both designs must control
cache lifetime, object naming, historical versions, partial content, and every asset in a
multi-file bundle—not only index.html.
Verify revocation, not just initial access
Run transitions, then read back the same stable path in a clean session:
| Transition | Expected result |
|---|---|
| Active → expired | Content path stops serving the artifact |
| Active → quarantined | Reader and raw asset paths deny access |
| Public/unlisted → protected | Unauthenticated path no longer returns bytes |
| Version 1 → version 2 | Stable reader resolves current version; guessed old path is denied |
| Active → deleted | Reader, entry file, and bundled assets become unavailable according to policy |
| Policy dependency unavailable | Service returns an unavailable/denied state, not stale content |
An HTTP 200 on the day of publication proves only that one path worked at that moment. Security depends on later state changes reaching every serving path.
Validate uploads and archives before serving them
Validation should confirm the artifact contract and reduce abuse; it should not be marketed as a guarantee that arbitrary JavaScript is harmless.
For a single HTML file, check at least:
- accepted extension and a plausible HTML marker;
- compressed/request and decoded byte limits;
- filename handling independent from object keys;
- disallowed remote runtime patterns required by the product policy;
- risk signals for triage without pretending a keyword list is a complete malware detector;
- final content type plus
X-Content-Type-Options: nosniffon delivery.
For an archive, add:
- a required root entry file such as
index.html; - normalized POSIX-style paths with no absolute paths, backslashes, null bytes, or
..traversal; - duplicate-path handling that makes the scanned bytes equal the served bytes;
- file-count, compressed-size, and total expanded-size ceilings checked before unbounded extraction;
- per-file and total plan limits after extraction;
- scanning of every executable or text asset, not only the root HTML;
- safe MIME mapping for every served extension.
OWASP calls out ZIP bombs and advises considering size after decompression. Archive validation also has a time-of-check/time-of-use requirement: if two entries normalize to the same storage key, the scanner and object writer must agree about which bytes win.
Scanning, isolation, and moderation solve different problems
| Layer | Good at | Does not prove |
|---|---|---|
| Structural validation | Rejecting malformed artifact shapes and known archive hazards | The JavaScript is benign |
| Blocklists and reputation | Catching known indicators and domains | A new or obfuscated payload is safe |
| Browser isolation | Removing application privileges from rendered code | The content cannot phish or exfiltrate data supplied inside it |
| Reporting and moderation | Responding after abusive content is observed | Every harmful artifact is found before first view |
| Quotas and rate limits | Reducing availability and cost abuse | A small artifact cannot be harmful |
Keep logging and retry states truthful. A scanner timeout is “unscanned” or “unavailable,” not a clean verdict.
Enforce lifecycle, monitoring, and response policy
A hosting system remains responsible after the first successful render. At minimum, document:
- visibility states and who may change them;
- expiry semantics and the clock used to enforce them;
- update, version-history, restore, and historical-object behavior;
- deletion from the reader, gateway, cache, object store, analytics, and backups;
- user report categories and a visible report path;
- automatic and manual quarantine states;
- operator alerts, triage ownership, appeal, and recovery;
- vulnerability disclosure and security-contact instructions.
Monitoring should distinguish a failed content fetch, policy dependency failure, blocked upload, risk flag, reputation hit, user report, and operator quarantine. Combining them into one “security scan failed” metric hides whether viewers are protected or the service is merely unavailable.
Review a hosting system with observable evidence
Ask for behavior you can inspect, not adjectives. This compact scorecard separates architecture claims from evidence.
| Review area | PASS evidence | FAIL signal |
|---|---|---|
| Trusted boundary | Account and content origins are named; content origin has no write/account APIs | Uploads share the session origin or the answer is unspecified |
| Runtime boundary | Exact iframe/CSP sandbox tokens are visible and justified | “Sandboxed” without configuration, or arbitrary raw open path |
| Storage boundary | Objects are private or every public path is policy-mediated | Bucket/object URLs bypass current visibility and moderation |
| Archive boundary | Traversal, duplicate paths, file count, compressed and expanded sizes are tested | Only filename extension is checked |
| Lifecycle boundary | Expiry, deletion, quarantine, visibility change, and old versions are read back | Policy exists only in dashboard UI |
| Failure boundary | Authorization dependency failure does not serve stale content | Policy errors silently fall back to public bytes |
| Response boundary | Report, quarantine, disclosure, and owner are documented | No route from observed abuse to revocation |
| Claim boundary | Limits and residual risks are explicit | “100% safe,” “risk-free,” or certification without scope/evidence |
Slidesfly implementation example
Slidesfly's product-specific facts remain on the canonical security architecture page.
As of the August 24, 2026 review, the implementation uses separate app and content domains, renders
decks in an iframe that omits allow-same-origin and allow-top-navigation, keeps R2 objects private,
checks current visibility, expiry, moderation, protection, and version state before public delivery,
and rejects unsafe archive paths and oversized expansion.
The review used repository code, tests, the live /security page, its response headers, the
agent-readable security.md, and a production request confirming that a write API
on the content domain returned 404. These checks validate the named paths and controls; they are not
a penetration test, independent certification, or promise that no defect exists.
For artifact preparation, use the HTML presentation preflight checklist. For the full hosting decision, start with the HTML presentation hosting guide. For a reproducible source and reader workflow, use the plain HTML Proof Pack.
Frequently asked questions
Is a sandboxed iframe enough to host untrusted HTML safely?
No. The sandbox controls browser capabilities inside one rendering path. You still need a separate low-privilege origin, mediated storage, upload and archive validation, access policy, revocation, reporting, moderation, availability controls, and tests for raw files, embeds, custom domains, downloads, and failure states.
Should I sanitize untrusted HTML or put it in an iframe?
Sanitize when the application needs safe rich text inside its trusted DOM and executable behavior is not part of the product. Use isolation when a complete HTML document must retain JavaScript and interaction. Sanitization changes the artifact; sandboxing preserves more behavior but requires a stronger origin, storage, and lifecycle boundary.
Can sandboxed HTML still make network requests?
Potentially, yes. Sandbox tokens and the same-origin policy do not automatically block every outbound request. Apply a Content Security Policy and deliberate CORS/server authorization, then test forms, fetches, images, media, fonts, beacons, links, and popups. Never pass secrets into the frame on the assumption that origin isolation prevents exfiltration.
Why omit allow-same-origin from the iframe sandbox?
Without allow-same-origin, the browser treats the framed resource as a special origin that fails
same-origin checks, limiting access to shared cookies, storage, and DOM. If an architecture needs
normal origin behavior, it should provide a demonstrably unique, low-privilege origin per artifact
and review the additional attack surface.
Does an unlisted URL make uploaded HTML safe?
No. Unlisted affects discovery, not code behavior or viewer authorization. Anyone who obtains the URL may be able to open it. Remove secrets before upload, use a verified protected policy for restricted audiences, and ensure that raw storage paths cannot bypass that policy.
How should a host behave when its policy service is down?
Fail closed for content authorization. Return an unavailable or denied response and retry safely; do not serve stale bytes because the policy lookup failed. Record the outage separately from a policy denial so operators can restore availability without weakening authorization.
Run the downloadable 34-check security review before accepting a hosting design. If the artifact itself is ready, continue to the publish-and-verify workflow and verify the returned reader URL in a clean browser session.
Continue with the Quickstart or browse all HTML presentation publishing guides.