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.
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.
| Feature | Outbound | Directive needed |
|---|---|---|
| Core editing (default setup) | Nothing. No fetch, XHR or beacon; assets come from your origin | 'self' only |
| Toolbar icons | Inline data: URIs | img-src data: |
Theme / plugin CSS + injected <style> | Your origin, plus runtime style elements | style-src-elem 'self' (+ nonce) |
| Text formatting | Inline style attributes on content | style-src-attr 'unsafe-inline' |
| Video embed | YouTube / Vimeo player iframes | frame-src for those hosts |
| Bookmark cards | Favicon service (Google by default; configurable, and can be disabled with an empty string) | img-src for that host, or turn it off |
| PDF export | Loads the ~900 KB renderer on first use — from your own assets, served next to the editor | 'self' (no third-party entry needed) |
| AI, collaboration, uploads | Only the endpoints you configure | connect-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.