cd /news/ai-products/safely-hosting-arbitrary-user-html-t… · home topics ai-products article
[ARTICLE · art-44555] src=dev.to ↗ pub= topic=ai-products verified=true sentiment=· neutral

Safely hosting arbitrary user HTML: the cookieless-origin sandbox pattern

ShareMyPage lets users publish HTML generated by LLMs like Claude or ChatGPT and share it behind per-page access control. To safely host arbitrary user HTML, the service serves untrusted content from a cookieless subdomain (view.) separate from the app (app.), uses short-lived signed URLs for access control, and relies on the iframe sandbox attribute without allow-same-origin to isolate scripts. This pattern prevents uploaded JavaScript from accessing the app's session or APIs.

read4 min views1 publishedJun 30, 2026

ShareMyPage lets people publish HTML, often generated by an LLM like Claude or ChatGPT, and share it behind per-page access control. So the core of the product is the one thing every security guide warns against: taking arbitrary HTML from users and serving it back, executing, in a browser.

Here is how I make that safe. The same approach works for anything that hosts user-supplied HTML: comment previews, email renderers, no-code builders, AI artifact viewers.

Rendering the HTML in an iframe on your own domain is broken. An iframe on app.yoursite.com

shares an origin with your app. If you grant allow-same-origin

, uploaded JavaScript can read document.cookie

, call your same-origin APIs with the user's session, and walk your DOM. And combining sandbox

with allow-same-origin

is the trap: together they hand the untrusted code back the origin you were trying to take away.

I serve untrusted HTML from a different origin than the app, view.

instead of app.

, and keep that origin cookieless. Three things work together.

First, the separate origin. Because content lives on its own domain, the same-origin policy now protects you instead of working against you. Scripts in the page can't reach the app origin at all.

Second, no cookies on the content origin. This isn't a hope, it's structural. The content route is left out of the auth middleware's matcher, so the origin never sets or receives an app session cookie. Even if isolation failed, there is no session to take.

Third, the sandbox attribute leaves out allow-same-origin

. The iframe gets a null origin. Scripts run, so interactive prototypes work, but they are cut off from anything that matters.

<iframe
  src={signedUrl}
  sandbox="allow-scripts allow-popups allow-forms"  // no allow-same-origin
  referrerPolicy="no-referrer"
/>

If the content origin is cookieless, how do you enforce access control there? With short-lived signed URLs.

The raw HTML sits in a private blob that is never publicly reachable, even if someone leaks the link. When an authorized user opens a page, the app origin, which does have the session, checks access and then signs a 60-second JWT and hands it to the iframe as its src

. The content route validates that signature and serves the bytes, with no session check of its own.

// content route, runs on the cookieless origin
const claim = await verifyContentToken(token); // verify JWT signature + audience
if (!claim) return new Response("Link expired.", { status: 401 });
const html = await fetchHtml(claim);           // private blob, server-side

Now two separate mechanisms answer two separate questions. A signed capability decides whether this person may see the page. Origin isolation decides whether this code can do damage. The 60-second expiry means a leaked iframe URL can't be replayed or passed around.

You might expect a tight Content-Security-Policy on the served HTML. I don't ship one, on purpose. AI-generated pages legitimately load fonts, images, and APIs, so the content CSP stays permissive (default-src *

). The iframe sandbox is what contains a malicious page, so that is where the strictness goes. What the CSP does lock down is who may frame the content: frame-ancestors

is pinned to the app origin, so nobody can embed your pages elsewhere. Every served response also carries noindex

, no-referrer

, nosniff

, and no-store

.

Tenant scoping lives in the query layer rather than the route layer. Every read and write is workspace-scoped in the database access code, with random unguessable IDs, so a missing check in one handler can't leak across tenants.

Secrets are hashed at rest. Page passwords use argon2id. API tokens use SHA-256, which is the right call rather than a lazy one: the token carries about 240 bits of entropy, so there is nothing to brute-force, and it gets checked on every API call. Tokens are shown once, scoped to a single user and workspace, and revocable.

Uploads, edits, and visibility changes are rate-limited and written to an audit log.

Put untrusted content on its own domain. The isolation comes from the origin, not from the sandbox attribute by itself.

Never combine allow-scripts

with allow-same-origin

for untrusted HTML. Together they are the same as having no sandbox.

Keep the content origin cookieless, and make it structural by leaving it out of your auth middleware, so there is nothing worth stealing even in the worst case.

Use signed URLs to decide who may view, and origin isolation to decide what the code can touch. They are different problems and deserve different mechanisms.

Be honest about where your real boundary is. Mine is the sandbox, so I don't pretend a permissive content CSP is doing the work.

It is built on Next.js 16, with content served from a dedicated cookieless route and 60-second signed URLs. It is live at sharemypage.app.

── more in #ai-products 4 stories · sorted by recency
── more on @sharemypage 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/safely-hosting-arbit…] indexed:0 read:4min 2026-06-30 ·