Skip to content
PolyFish CSS

Why we removed the sandbox

It was the most convincing demo on the site, and it weakened a header that protects every page on it.

· ~4 min read · 0.18.3 in the changelog

Two weeks ago we shipped a live sandbox — type markup, watch the real framework render it in an iframe. It worked well, people used it, and this release takes it out again, along with the iframe device previewer on the responsive page. Here is the reasoning, because "we removed a thing you liked" deserves one.

What it cost

To frame our own pages, the site's policy had to move from frame-ancestors 'none' to 'self', X-Frame-Options from DENY to SAMEORIGIN, and gain a frame-src directive. The frame itself was sandboxed without allow-scripts, so typed markup could not execute. The problem was not the sandbox. It was that a header protecting every page on the site had been relaxed for the benefit of one.

Postscript: the header we relaxed without noticing

The principle in this post — a header that protects every page should not be weakened for one of them — turned out to have a second edge we had not seen. We were careful about the headers we loosened deliberately, and careless about the one we tightened.

The site sends Cross-Origin-Embedder-Policy: require-corp, which reads as maximal caution. It also blocks any cross-origin resource that does not send its own CORP or CORS header, and the self-hosted analytics script does not. So from the day analytics shipped, Chromium refused it with ERR_BLOCKED_BY_RESPONSE and we were collecting nothing from the browsers most of our readers use. We found it because the browser suite now serves pages through a server that sends the production headers, rather than a bare static file server that sends none.

The header is now credentialless, which keeps the isolation and fetches such resources without cookies. The broader point is the mirror image of this post's: a security header that is stricter than your site can support does not fail loudly. It fails by quietly removing something, and unless something in your pipeline actually speaks the policy back to you, the first report comes from a user or from nobody.

What replaced it

The responsive page now has a stage you can drag by its corner, with real components inside responding through container queries. It is honest about the difference: those components respond to the stage, not to your window, and to watch the framework's own breakpoints you resize the browser. That is a smaller demo of a real feature instead of a more convincing one of a different feature.

A new build check enforces the stance in both directions: it fails if a page gains an iframe while the policy refuses framing, and fails if the policy allows framing while no page needs it.

What this post does not cover

  • A general argument against iframes. This is about one sandbox on one documentation site.
  • Content-Security-Policy tuning beyond the directives this change touched.

Checked against container queries. Figures are measured from the build they describe.

What this doesn’t cover

Removing the iframe removed a real capability: you can no longer paste markup and watch it render. Nothing here replaces that, and if you want it, a local file and a browser still work. The header change also only covers the pages we serve; a copy of this framework behind your own server inherits your policy, not ours.

Sources: MDN on frame-ancestors and MDN on container queries. The header values quoted are from the nginx snippet this site actually serves. No code blocks in this post.

Security CSP Iframes Container Queries Release CSS