diff --git a/Classes/EventListener/PolicyMutatedEventListener.php b/Classes/EventListener/PolicyMutatedEventListener.php index c786e6d..7049261 100644 --- a/Classes/EventListener/PolicyMutatedEventListener.php +++ b/Classes/EventListener/PolicyMutatedEventListener.php @@ -9,9 +9,15 @@ use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive; use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyMutatedEvent; use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyPreparedEvent; +use TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy; +use TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashValue; +use TYPO3\CMS\Core\Security\ContentSecurityPolicy\RawValue; use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword; +use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme; use TYPO3\CMS\VisualEditor\Service\EditModeService; +use function str_starts_with; + final readonly class PolicyMutatedEventListener { public function __construct( @@ -55,14 +61,104 @@ public function __invoke(PolicyMutatedEvent $event): void $policy = $event->getCurrentPolicy(); + // we add all necessary CSP rules, so even if someone sets all Directives to 'none' we have a working Editor. + // add style-src 'unsafe-inline' to allow a working ckeditor in the frontend. - $policy = $policy->reduce(Directive::StyleSrc, SourceKeyword::nonceProxy); // to allow 'unsafe-inline' we first need to remove the nonces + $policy = $policy->reduce(Directive::StyleSrc, SourceKeyword::nonceProxy, SourceKeyword::none); // to allow 'unsafe-inline' we first need to remove the nonces $policy = $policy->extend(Directive::StyleSrc, SourceKeyword::self, SourceKeyword::unsafeInline); if ($policy->has(Directive::StyleSrcAttr)) { - // add the same to StyleSrcAttr if that is present - $policy = $policy->reduce(Directive::StyleSrcAttr, SourceKeyword::nonceProxy); // to allow 'unsafe-inline' we first need to remove the nonces - $policy = $policy->extend(Directive::StyleSrcAttr, SourceKeyword::unsafeInline); + // add the same to StyleSrcAttr if that is present (fallback chain otherwise) + $policy = $policy->reduce(Directive::StyleSrcAttr, SourceKeyword::nonceProxy, SourceKeyword::none); // to allow 'unsafe-inline' we first need to remove the nonces + $policy = $policy->extend(Directive::StyleSrcAttr, SourceKeyword::unsafeInline); // no self as that is not a thing for style-src-attr + } + + if ($policy->has(Directive::StyleSrcElem)) { + // add the same to StyleSrcElem if that is present (fallback chain otherwise) + $policy = $policy->reduce(Directive::StyleSrcElem, SourceKeyword::nonceProxy, SourceKeyword::none); // to allow 'unsafe-inline' we first need to remove the nonces + $policy = $policy->extend(Directive::StyleSrcElem, SourceKeyword::self, SourceKeyword::unsafeInline); + } + + $policy = $policy->reduce(Directive::ScriptSrc, SourceKeyword::nonceProxy, SourceKeyword::none); + $policy = $policy->extend(Directive::ScriptSrc, SourceKeyword::self, SourceKeyword::unsafeInline); + + if ($policy->has(Directive::ScriptSrcElem)) { + $policy = $policy->reduce(Directive::ScriptSrcElem, SourceKeyword::nonceProxy, SourceKeyword::none); + $policy = $policy->extend(Directive::ScriptSrcElem, SourceKeyword::self, SourceKeyword::unsafeInline); + } + + + if ($policy->has(Directive::ImgSrc)) { + $policy = $policy->reduce(Directive::ImgSrc, SourceKeyword::none); + $policy = $policy->extend(Directive::ImgSrc, SourceKeyword::self, SourceScheme::data); + } + + if ($policy->has(Directive::FontSrc)) { + $policy = $policy->reduce(Directive::FontSrc, SourceKeyword::none); + $policy = $policy->extend(Directive::FontSrc, SourceKeyword::self); + } + + if ($policy->has(Directive::FrameAncestors)) { + $policy = $policy->reduce(Directive::FrameAncestors, SourceKeyword::none); + $policy = $policy->extend(Directive::FrameAncestors, SourceKeyword::self); + } + + if ($policy->has(Directive::ConnectSrc)) { + $policy = $policy->reduce(Directive::ConnectSrc, SourceKeyword::none); + $policy = $policy->extend(Directive::ConnectSrc, SourceKeyword::self); + } + + if ($policy->has(Directive::TrustedTypes)) { + $policy = $policy->reduce(Directive::TrustedTypes, SourceKeyword::none); + $policy = $policy->extend(Directive::TrustedTypes, new RawValue('lit-html')); + } + + if ($policy->has(Directive::RequireTrustedTypesFor)) { + $policy = $policy->remove(Directive::RequireTrustedTypesFor); + } + + if ($policy->has(Directive::Sandbox)) { + $policy = $policy->remove(Directive::Sandbox); + } + + // filter out all hashs and nonces + foreach (Directive::cases() as $directive) { + if (!$policy->has($directive)) { + continue; + } + + $sources = []; + foreach ($policy->get($directive)->sources ?? [] as $source) { + if ($source instanceof HashValue) { + continue; + } + + if ($source instanceof HashProxy) { + continue; + } + + if ($source instanceof RawValue) { + if (str_starts_with((string)$source, "'sha256-")) { + continue; + } + + if (str_starts_with((string)$source, "'sha384-")) { + continue; + } + + if (str_starts_with((string)$source, "'sha512-")) { + continue; + } + + if (str_starts_with((string)$source, "'nonce-")) { + continue; + } + } + + $sources[] = $source; + } + + $policy = $policy->set($directive, ...$sources); } $event->setCurrentPolicy($policy);