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-Type that every browser supports and that carries no active content. When uncertain, application/octet-stream is the safe default.
  • Always append the response headers that force full browser isolation of the payload.
Response Header Purpose

X-Content-Type-Options: nosniff

Prevents content sniffing

Content-Disposition: attachment; filename="download"

Triggers a download rather than rendering

Content-Security-Policy: sandbox

Sandboxes the content as if it was served on a separate domain

Content-Security-Policy: default-src ‘none'

Disables JavaScript execution (and inclusion of any subresources)

Cross-Origin-Resource-Policy: same-site

Prevents the page from being included cross-site
That header combination restricts the response to two legitimate uses: loading as a subresource by the application or downloading as a file by the user. Multiple layers guard against browser quirks, including both a CSP 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: sandbox header 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 with Cross-Origin-Opener-Policy: same-origin plus Cross-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.com is on the PSL, foo.exampleusercontent.com and bar.exampleusercontent.com are fully isolated from each other.
  • Route every URL matching *.exampleusercontent.com/shim to a single static HTML file. That shim listens for message events and renders whatever content it receives.
  • The application opens an iframe or dialog pointing at $RANDOM_VALUE.exampleusercontent.com/shim and uses postMessage to 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.