Security

Content-Security-Policy for rich text editors

Editors inject stylesheets, set inline style attributes, embed iframes, and sometimes fetch libraries mid-session. Here is how to work out the policy you actually need instead of reaching for unsafe-inline.

8 min read

A rich text editor is one of the harder components to put behind a strict Content-Security-Policy. It injects stylesheets, it renders user HTML, it may embed third-party iframes, and some features load libraries on demand. Get the policy wrong and the failure is rarely a clean error — a button silently does nothing, or an export produces a blank file.

This is how to work out the policy your editor actually needs, rather than reaching for unsafe-inline everywhere and hoping.

Step one: enumerate, do not guess

Vendor documentation is often incomplete about outbound calls, and the answer changes per feature. Enumerate empirically: instrument fetch, XMLHttpRequest and sendBeacon before the editor loads, exercise every feature you intend to ship, then read the log. The method is written up in a separate article.

Do this per feature, because the interesting requests are conditional. An editor can be completely quiet at rest and still load a library the first time someone exports a PDF.

The directives that actually matter

style-src

Editors inject stylesheets at runtime — theme CSS, plugin CSS, and often a <style> element created in JavaScript for view features. They also set inline styleattributes constantly, because that is what “make this text red” means in a contenteditable.

Note the split: CSP3 style-src-attr governs inline style="" attributes, while style-src-elem governs <style> and <link>. A nonce covers injected <style> elements but does not cover style attributes. If you need user formatting to work, style-src-attr 'unsafe-inline'is usually unavoidable — and it is a much narrower concession than blanket unsafe-inline, because style attributes cannot execute script in any modern browser.

img-src

Two things beyond your own origin are common. Editors frequently inline toolbar icons as data: URIs, so img-src data:is usually required. And if users paste or upload images, decide deliberately whether remote images are allowed at all — img-src * is a privacy leak (it lets a pasted image beacon your users), while restricting to your own origin means you must proxy or re-host on paste.

frame-src

Video embed features insert third-party iframes — typically https://www.youtube.com/embed/… and https://player.vimeo.com/…. If you allow the feature, allow the frames; if you do not want the frames, turn the feature off rather than leaving a button that fails silently.

script-src

This is the one that surprises people. Some editor features load a third-party library on first use rather than bundling it — PDF export is the usual example, because the rendering libraries are large. That is a runtime script-src requirement that will not appear until someone clicks the button.

Any feature that pulls a script from a CDN at runtime is also a supply-chain dependency. Check whether the URL is version-pinned. A URL that points at a branch rather than a tagged release can change underneath you, and it is worth self-hosting that asset from your own origin instead — which also removes the script-src exception entirely.

A worked example

Here is the actual outbound surface of RichTextEditor, feature by feature, from instrumenting a default self-hosted setup and then each optional feature in turn.

FeatureOutboundDirective needed
Core editing (default setup)Nothing. No fetch, XHR or beacon; assets come from your origin'self' only
Toolbar iconsInline data: URIsimg-src data:
Theme / plugin CSS + injected <style>Your origin, plus runtime style elementsstyle-src-elem 'self' (+ nonce)
Text formattingInline style attributes on contentstyle-src-attr 'unsafe-inline'
Video embedYouTube / Vimeo player iframesframe-src for those hosts
Bookmark cardsFavicon service (Google by default; configurable, and can be disabled with an empty string)img-src for that host, or turn it off
PDF exportLoads the ~900 KB renderer on first use — from your own assets, served next to the editor'self' (no third-party entry needed)
AI, collaboration, uploadsOnly the endpoints you configureconnect-src for your own services

The shape worth noticing: the base editor needs nothing beyond 'self', and every remaining external entry belongs to an optional feature you can disable or reconfigure. That is the property to test for in any editor you are evaluating — not “does it work under CSP” but “which exceptions does each feature cost me, and can I opt out?”

PDF export is a worked example of getting this wrong and then fixing it. It used to fetch its renderer from a GitHub raw-CDN URL that tracked a branch rather than a release — which meant a third-party script-srcentry, a hard failure in air-gapped deployments, and a dependency that could change with no version pin. The bundle is now pinned and shipped with the editor’s own assets, so exporting a PDF makes no external request at all. If you would rather serve it from your own CDN or a versioned asset path, html2pdfScriptUrl overrides the location.

A starting policy

Tighten from here rather than loosening into it. Drop the lines for features you do not ship, and replace CDN hosts with your own origin wherever you self-host the asset.

Content-Security-Policy:
  default-src 'self';
  script-src 'self';                    /* + CDN host only if PDF export is used */
  style-src-elem 'self' 'nonce-{RANDOM}';
  style-src-attr 'unsafe-inline';       /* required for user text formatting */
  img-src 'self' data: blob:;           /* add upload/CDN origins as needed */
  frame-src https://www.youtube.com https://player.vimeo.com;
  connect-src 'self';                   /* + your AI / collab / upload endpoints */
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'self';

Roll it out in report-only first

Editor CSP failures are quiet. Deploy with Content-Security-Policy-Report-Only and a report-toendpoint, leave it for a full usage cycle, and let real users find the feature you forgot. Exercise the long tail deliberately — export, print, embed, paste from Word, upload an image — because those are exactly the paths that reach for something extra.

Finally: CSP is a second line of defence, not a substitute for sanitising the HTML your editor stores and re-renders. It limits the damage of an XSS payload that already got through; it does not stop the payload being stored.


RichTextEditor is a perpetual-licence JavaScript editor — one purchase, self-hosted, no metered editor loads. Download the evaluation or see how it compares.