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 notcover style attributes. If you need user formatting to work, some inline-style concession is unavoidable — but do not assume the attribute directive is the one that buys it. Which directive a browser blames depends on how the editor applies the style, and that is a question to answer by measuring rather than by reading the spec. For this editor, measured, style-src-attr turns out to cover almost none of it; the numbers are below.
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 styling applied at runtime | style-src 'self' 'unsafe-inline' (measured; see below) |
| 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 'self' 'unsafe-inline'; /* measured: see the note below */
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';What this editor actually needs, measured
The split above is how CSP3 is specified. It is not, for this editor, how it behaves — so here are the numbers rather than the theory. Each policy was served to a page running the full editor and the console refusals were counted:
| Policy | Refusals | Editor |
|---|---|---|
style-src 'self' | 49 | renders, console full of errors |
+ style-src-attr 'unsafe-inline' | 48 | removes exactly one refusal |
+ style-src-elem 'unsafe-inline' | 8 | toolbar collapses — see the trap below |
style-src 'self' 'unsafe-inline' | 0 | correct |
So style-src-attr 'unsafe-inline'is not the narrow concession it looks like here: almost all of this editor’s runtime styling is applied in ways the browser attributes to style-src, not to the attribute directive. An earlier version of this page recommended it, and that recommendation removed one refusal out of forty-nine. Corrected 3 September 2026.
The trap in that third row. Naming style-src-elem replaces style-src for elements entirely — it does not add to it. Writing style-src-elem 'unsafe-inline' without also listing 'self' therefore blocks the theme stylesheet itself, and the editor renders with no styling at all: a 43,000-pixel column of unstyled buttons. If you specify the granular directives, list every source each one needs.
The editor still renders under the strict policy in row one, which is what makes this worth measuring rather than assuming: a CSP that is quietly wrong here does not produce a blank page, it produces a working toolbar and forty-nine console errors that nobody reads until a feature misbehaves.
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.