Section 10: Strangers' HTML on Your Domain - Security, Abuse & Content Safety
Executive Summary: The platform takes text from 2M monthly writers it has never met and serves it, under its own domain, to 25M readers a day. That makes the writer the most dangerous user in the system. Each threat below gets a mechanism, a rejected alternative and a named residual risk. The structured-document decision (D13), already visible as the doc field in Section 4's API, turns out to be the strongest security control in the design.
The interviewer turns the marker around. "So far your attackers have been traffic and failure. Now the attacker has an account. Maya is honest. The next writer isn't. What does he do with your editor?"
He writes a post, and the platform serves it from its own domain, cached at every edge for 24 hours.
Stored XSS: Never Accept HTML
You: "The editor never sends HTML. It sends the structured JSON document from decision D13, the ProseMirror-style tree we autosave. The server validates it against a schema of known nodes (paragraph, heading, list, code block, image, embed) and marks (link, bold, italic). There's no raw-HTML node, so there's nowhere to put a <script>. The renderer emits only tags it knows and escapes every text node."
Interviewer: "Why not accept HTML and run a good sanitizer? Most CMSs do."
You: "Sanitizer bypasses are a recurring CVE class. With an allow-list renderer I'm not choosing what to strip from an attacker's HTML; I'm generating HTML from a format that can't express anything dangerous. I don't see a trigger that flips this."
The format's edges still need rules:
- Links: only
http,httpsandmailtoare accepted.javascript:anddata:URLs fail validation. Every outbound link getsrel="nofollow ugc noopener". - Embeds: only allow-listed providers, resolved via oEmbed, rendered in a sandboxed iframe on a separate origin, with no access to our cookies or DOM.
- CSP:
script-srcallows only our versioned bundles by hash, not by nonce, because a nonce cached for every reader is no nonce at all. Addobject-src 'none'andbase-uri 'none', and even a renderer bug doesn't execute. - Cookies and origins: the editor, sign-in and
/v1APIs live on a separate origin (app./api.). The session cookie isHttpOnly; Secure; SameSite=Laxand host-only there, so script that somehow ran on an article page could neither read the session nor make credentialed same-origin calls. Personal state comes from/me, a credentialed CORS call to the API origin, so the cached HTML holds no secrets.
Key Insight: The protection comes from what the format can't express. A sanitizer has to beat every new trick. An allow-list renderer only has to get its handful of node types right, and CSP backs it up.
Quick check before moving on:
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson