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.