Skip to content

PolicyMutatedEventListener doesn't actually relax the CSP for CKEditor5 in the frontend #115

Description

@p2media

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:

  1. 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.

  2. 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

  1. Configure a site's csp.yaml with:
    behavior:
      useNonce: false
      useHash: true
  2. Install friendsoftypo3/visual-editor (tested with v1.9.0 on TYPO3 v14.3.5).
  3. Log in as a backend user, open a page in the frontend edit mode (?editMode=1).
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions