fix(sanitize): allow trusted-host iframe embeds in post/page body content - #369
fix(sanitize): allow trusted-host iframe embeds in post/page body content#369asachs01 wants to merge 1 commit into
Conversation
…tent base.outlet's `html` prop (the markdown-rendered entry body every post uses) was declared type `richtext`, so escapeProps() ran it through DOMPurify's RICHTEXT_CONFIG — an allowlist built for short-form fields (p/strong/em/a/ul/li) with no img, video, table, or iframe. marked passes raw HTML blocks through untouched, so a pasted YouTube <iframe> survives markdown parsing fine and is then silently stripped at this later sanitize step. Reported by WYRE (2 posts with embedded YouTube videos not rendering). Adds a new `richtextBody` control type, used only by base.outlet's `html` prop, sanitized via a new POST_BODY_CONFIG: the same safe formatting tags as RICHTEXT_CONFIG plus img/video/table (what a real post body markdown-renders to) plus iframe — scoped to a trusted-host allowlist (youtube.com, youtube-nocookie.com, subdomain-aware) via a DOMPurify uponSanitizeElement hook that removes the whole element (not just the src) on any non-match, including lookalike-host attempts (youtube.com.evil.com, evilyoutube.com — tested). Every other richtext-typed field in the CMS is unaffected — kept as a distinct control type rather than widening RICHTEXT_CONFIG globally, so short-form fields elsewhere don't gain a wider attack surface than they need. Full suite: 6611 tests passing (0 fail), tsc clean, lint clean.
|
Tried this against a Webflow migration where the post bodies are HTML, and The config allows <video controls poster="/uploads/thumb.avif">
<source src="/uploads/clip.mp4" type="video/mp4">
</video>publishes as a bare Suggest adding Separately, and maybe worth a line in this PR's description: with the current narrow Context: found while doing an AI-assisted migration of a ~26-post site from Webflow into Instatic, so the findings come from that codebase rather than a hand-written repro. |
Summary
base.outlet'shtmlprop (the markdown-rendered post/page body) is typedrichtext, soescapeProps()sanitizes it throughRICHTEXT_CONFIG— an allowlist built for short marketing copy with noimg/video/table/iframe.markedpasses raw HTML blocks through untouched during markdown parsing, so a pasted YouTube<iframe>(or any other embed) survives parsing and then gets silently stripped at the later sanitize step — with no error or warning surfaced to the author.Confirmed there's no existing zero-code path around this: the Tiptap post-body editor has no video/embed insertion feature ("Add Media" opens the media-library file picker, not a URL-embed), and
base.video(the module that could otherwise host a YouTube embed) is page-builder-canvas-only — it has no bridge into markdown post-body fields.Change
richtextBodycontrol type, distinct fromrichtext, used only bybase.outlet.POST_BODY_CONFIGDOMPurify config: allowsimg/video/table/iframe, withiframescoped to a trusted-host allowlist (youtube.com,youtube-nocookie.com, subdomain-aware) via anuponSanitizeElementhook.youtube.com.evil.com,evilyoutube.comboth correctly rejected) to guard the allowlist logic itself.richtext-typed field (short-copy fields elsewhere in the module system) is unaffected — this only changes the config used bybase.outlet's body prop.Test plan
tsccleaneslintcleanHappy to adjust the trusted-host list or expose it as a config option if that's preferred upstream — went with a hardcoded YouTube-only allowlist since that's the immediate need, but a configurable allowlist would be a small follow-up if there's appetite for embeds from other providers.