Serving untrusted user content without the classic sandbox tradeoffs
Nearly every web application that accepts user input eventually has to display it back. The spectrum runs from simple profile photos to fully user-controlled HTML, and each step up in complexity raises the security bar. Historically, the standard mitigation was a dedicated sandbox domain: serve all untrusted content from exampleusercontent.com while the app itself lives on example.com. Because the two origins are cross-site, malicious payloads can't reach the application's cookies or DOM.
That pattern works, and it has served the industry for years. But it carries two structural costs. First, protecting a user's content from being read by others requires authentication on the sandbox domain, which by design doesn't share cookies with the main site. That forces either capability URLs or separate authentication cookies for the sandbox domain — the latter a poor fit for a web where browsers increasingly block cross-site cookies by default. Second, a shared sandbox domain isolates user content from the application but not from other users' content, so a malicious upload could still attack same-origin data stored alongside it.
Two newer approaches address these gaps, and both are already in production use at Google.
Inactive content: no sandbox domain required
For content that is not HTML or JavaScript — images, downloads, files of any kind — an isolated domain is no longer necessary. Two header rules make the response safe to serve from the application's own origin:
- Always send a well-known
Content-Typethat every browser supports and that carries no active content. When uncertain,application/octet-streamis the safe default. - Always append the response headers that force full browser isolation of the payload.
| Response Header | Purpose |
|---|---|
|
Prevents content sniffing |
|
Triggers a download rather than rendering |
|
Sandboxes the content as if it was served on a separate domain |
|
Disables JavaScript execution (and inclusion of any subresources) |
|
Prevents the page from being included cross-site |
sandbox directive and a restrictive default-src, so injection and isolation bugs are unlikely to surface.
Teams wanting extra depth can layer on further hardening:
- Add an
X-Content-Security-Policy: sandboxheader for legacy IE11 compatibility. - Send
Content-Security-Policy: frame-ancestors 'none'to prevent the endpoint from being embedded anywhere. - Move user content to an isolated subdomain — Google, for instance, serves it from domains like
product.usercontent.google.com— and enable cross-origin isolation withCross-Origin-Opener-Policy: same-originplusCross-Origin-Embedder-Policy: require-corp.
Active content: a new take on the sandbox domain
Active content — HTML, SVG images, anything the browser will parse and execute — is a harder problem. The simplest option is still the Content-Security-Policy: sandbox header, which tells the browser to treat the response as an isolated document. Browser-level process isolation for sandboxed frames is not universal today, but ongoing work on process models should close that gap. For teams whose threat model excludes SpectreJS-style attacks and compromised renderer processes, CSP sandbox is likely sufficient.
Google's more robust approach modernizes the sandbox domain concept to address both weaknesses of the original:
- Register the sandbox domain on the public suffix list, so subdomains are cross-site by construction. Once
exampleusercontent.comis on the PSL,foo.exampleusercontent.comandbar.exampleusercontent.comare fully isolated from each other. - Route every URL matching
*.exampleusercontent.com/shimto a single static HTML file. That shim listens formessageevents and renders whatever content it receives. - The application opens an iframe or dialog pointing at
$RANDOM_VALUE.exampleusercontent.com/shimand usespostMessageto deliver the untrusted content for rendering. - Before display, the shim wraps the rendered content in a Blob and places it inside a sandboxed iframe.
The outcome is that every piece of user content lands on its own unique site, isolated from the application and from all other user content. Because the application itself fetches and forwards the data, capability URLs become unnecessary, and the whole flow works under third-party cookie blocking.
Together these patterns let products retire classic shared sandbox domains such as googleusercontent.com in favor of isolation schemes that are both stricter and more compatible with modern browser privacy defaults. Google has already migrated multiple products to these designs, with additional migrations in progress.



