Summary
Classes/EventListener/PolicyMutatedEventListener.php is meant to relax the site's Content-Security-Policy so CKEditor5 can run inside the frontend edit mode. It doesn't work, for two independent reasons:
-
Policy::mutate() is not a mutating method despite its name — it's an immutable value object and returns a new Policy instance. The listener calls it and discards the return value instead of feeding it back into the event, so the whole listener is a no-op:
// Classes/EventListener/PolicyMutatedEventListener.php
$mutation = new Mutation(MutationMode::Reduce, Directive::StyleSrc, SourceKeyword::nonceProxy);
$event->getCurrentPolicy()->mutate($mutation); // <- return value discarded
$mutation = new Mutation(MutationMode::Extend, Directive::StyleSrc, SourceKeyword::self, SourceKeyword::unsafeInline);
$event->getCurrentPolicy()->mutate($mutation); // <- return value discarded
$event->setCurrentPolicy(...) is never called, so PolicyMutatedEvent::getCurrentPolicy() still returns the completely unmodified policy afterwards.
-
Even with the return value wired up correctly, the listener only targets Directive::StyleSrc / Directive::StyleSrcAttr. In practice, under a hash-based CSP policy (useHash: true, useNonce: false — the setup TYPO3 core itself recommends for cacheable frontend responses, see Feature-100887), the browser reports violations on style-src-elem and script-src-elem, not style-src/style-src-attr:
- CKEditor5 injects
<style> elements dynamically at runtime (ckeditor5-engine, -core, -ui, -widget, -clipboard, -link, -editor-classic all do this for their own theming) — this is blocked by style-src-elem, and its content can never be pre-computed as a CSP hash since it's generated client-side.
- TYPO3 core's own
PageRenderer::renderMainJavaScriptLibraries() emits an inline var TYPO3 = Object.assign(TYPO3 || {}, ...) bootstrap script for the frontend whenever any inline settings/labels are registered (which visual-editor's own EditModeService::loadLanguageLabelsInline() triggers) — this script gets neither a nonce nor a CSP hash registered for it on the frontend code path, so it's blocked by script-src-elem under a hash-based policy.
Steps to reproduce
- Configure a site's
csp.yaml with:
behavior:
useNonce: false
useHash: true
- Install
friendsoftypo3/visual-editor (tested with v1.9.0 on TYPO3 v14.3.5).
- Log in as a backend user, open a page in the frontend edit mode (
?editMode=1).
- Open the browser console.
Expected
CKEditor5 loads and renders correctly in the frontend edit mode; PolicyMutatedEventListener relaxes the policy enough to allow it.
Actual
Console shows CSP violations, e.g.:
Executing inline script violates the following Content Security Policy directive 'script-src-elem 'self' 'sha256-...' 'report-sample' 'sha256-...' 'sha256-...''. Either the 'unsafe-inline' keyword, a hash (...), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.
ckeditor5-engine.js:1 Applying inline style violates the following Content Security Policy directive 'style-src-elem 'self' 'report-sample''. Either the 'unsafe-inline' keyword, a hash (...), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.
(same for ckeditor5-core.js, ckeditor5-ui.js, ckeditor5-widget.js, ckeditor5-clipboard.js, ckeditor5-link.js, ckeditor5-editor-classic.js)
Suggested fix
final readonly class PolicyMutatedEventListener
{
public function __construct(private EditModeService $editModeService)
{
}
#[AsEventListener]
public function __invoke(PolicyMutatedEvent $event): void
{
$request = $event->request ?? $GLOBALS['TYPO3_REQUEST'] ?? null;
if (!$request) {
return;
}
if (!$this->editModeService->isEditMode($request)) {
return;
}
$currentPolicy = $event->getCurrentPolicy();
// add style-src 'unsafe-inline' to allow a working ckeditor in the frontend.
$currentPolicy = $currentPolicy->mutate(new Mutation(MutationMode::Reduce, Directive::StyleSrc, SourceKeyword::nonceProxy));
$currentPolicy = $currentPolicy->mutate(new Mutation(MutationMode::Extend, Directive::StyleSrc, SourceKeyword::self, SourceKeyword::unsafeInline));
// CKEditor5 injects <style> elements dynamically at runtime; their content can never
// be hashed ahead of time, so style-src-elem needs the same relaxation.
$currentPolicy = $currentPolicy->mutate(new Mutation(MutationMode::Set, Directive::StyleSrcElem, SourceKeyword::self, SourceKeyword::unsafeInline));
// Core's own inline "TYPO3.settings" bootstrap script (emitted once JS modules/inline
// labels are registered, as EditModeService does) isn't nonce'd or hash-registered on
// the frontend code path under a hash-based policy.
$currentPolicy = $currentPolicy->mutate(new Mutation(MutationMode::Set, Directive::ScriptSrcElem, SourceKeyword::self, SourceKeyword::unsafeInline));
if ($currentPolicy->get(Directive::StyleSrcAttr)) {
$currentPolicy = $currentPolicy->mutate(new Mutation(MutationMode::Reduce, Directive::StyleSrcAttr, SourceKeyword::nonceProxy));
$currentPolicy = $currentPolicy->mutate(new Mutation(MutationMode::Extend, Directive::StyleSrcAttr, SourceKeyword::unsafeInline));
}
$event->setCurrentPolicy($currentPolicy);
}
}
Environment
friendsoftypo3/visual-editor: 1.9.0
typo3/cms-core: 14.3.5
- PHP: 8.4
- Site CSP:
useHash: true, useNonce: false
Summary
Classes/EventListener/PolicyMutatedEventListener.phpis meant to relax the site's Content-Security-Policy so CKEditor5 can run inside the frontend edit mode. It doesn't work, for two independent reasons:Policy::mutate()is not a mutating method despite its name — it's an immutable value object and returns a newPolicyinstance. The listener calls it and discards the return value instead of feeding it back into the event, so the whole listener is a no-op:$event->setCurrentPolicy(...)is never called, soPolicyMutatedEvent::getCurrentPolicy()still returns the completely unmodified policy afterwards.Even with the return value wired up correctly, the listener only targets
Directive::StyleSrc/Directive::StyleSrcAttr. In practice, under a hash-based CSP policy (useHash: true,useNonce: false— the setup TYPO3 core itself recommends for cacheable frontend responses, see Feature-100887), the browser reports violations onstyle-src-elemandscript-src-elem, notstyle-src/style-src-attr:<style>elements dynamically at runtime (ckeditor5-engine, -core, -ui, -widget, -clipboard, -link, -editor-classic all do this for their own theming) — this is blocked bystyle-src-elem, and its content can never be pre-computed as a CSP hash since it's generated client-side.PageRenderer::renderMainJavaScriptLibraries()emits an inlinevar TYPO3 = Object.assign(TYPO3 || {}, ...)bootstrap script for the frontend whenever any inline settings/labels are registered (which visual-editor's ownEditModeService::loadLanguageLabelsInline()triggers) — this script gets neither a nonce nor a CSP hash registered for it on the frontend code path, so it's blocked byscript-src-elemunder a hash-based policy.Steps to reproduce
csp.yamlwith:friendsoftypo3/visual-editor(tested with v1.9.0 on TYPO3 v14.3.5).?editMode=1).Expected
CKEditor5 loads and renders correctly in the frontend edit mode;
PolicyMutatedEventListenerrelaxes the policy enough to allow it.Actual
Console shows CSP violations, e.g.:
(same for ckeditor5-core.js, ckeditor5-ui.js, ckeditor5-widget.js, ckeditor5-clipboard.js, ckeditor5-link.js, ckeditor5-editor-classic.js)
Suggested fix
Environment
friendsoftypo3/visual-editor: 1.9.0typo3/cms-core: 14.3.5useHash: true,useNonce: false