diff --git a/typo3/sysext/backend/Classes/Middleware/ContentSecurityPolicyHeaders.php b/typo3/sysext/backend/Classes/Middleware/ContentSecurityPolicyHeaders.php
new file mode 100644
index 0000000000000000000000000000000000000000..319d9cea44bb8f16296edf29e9f41fe345459e92
--- /dev/null
+++ b/typo3/sysext/backend/Classes/Middleware/ContentSecurityPolicyHeaders.php
@@ -0,0 +1,69 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Backend\Middleware;
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use Psr\Log\LoggerInterface;
+use TYPO3\CMS\Core\Configuration\Features;
+use TYPO3\CMS\Core\Core\RequestId;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyProvider;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
+
+/**
+ * Adds Content-Security-Policy headers to response.
+ *
+ * @internal
+ */
+final class ContentSecurityPolicyHeaders implements MiddlewareInterface
+{
+    public function __construct(
+        private readonly Features $features,
+        private readonly RequestId $requestId,
+        private readonly LoggerInterface $logger,
+        private readonly PolicyProvider $policyProvider,
+    ) {
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        $request = $request->withAttribute('nonce', $this->requestId->nonce);
+        $response = $handler->handle($request);
+
+        if (!$this->features->isFeatureEnabled('security.backend.enforceContentSecurityPolicy')) {
+            return $response;
+        }
+
+        $scope = Scope::backend();
+        if ($response->hasHeader('Content-Security-Policy') || $response->hasHeader('Content-Security-Policy-Report-Only')) {
+            $this->logger->info('Content-Security-Policy not enforced due to existence of custom header', [
+                'scope' => (string)$scope,
+                'uri' => (string)$request->getUri(),
+            ]);
+            return $response;
+        }
+
+        $policy = $this->policyProvider->provideFor($scope);
+        if ($policy->isEmpty()) {
+            return $response;
+        }
+        return $response->withHeader('Content-Security-Policy', (string)$policy);
+    }
+}
diff --git a/typo3/sysext/backend/Classes/Template/PageRendererBackendSetupTrait.php b/typo3/sysext/backend/Classes/Template/PageRendererBackendSetupTrait.php
index 7d5f6873a49edd222af8d6974a6a7e8e1b2643dc..22d0328ab125367b34302a2be7420d6e6ad2f385 100644
--- a/typo3/sysext/backend/Classes/Template/PageRendererBackendSetupTrait.php
+++ b/typo3/sysext/backend/Classes/Template/PageRendererBackendSetupTrait.php
@@ -55,6 +55,10 @@ trait PageRendererBackendSetupTrait
         $pageRenderer->setLanguage($languageService->lang);
         $pageRenderer->setMetaTag('name', 'viewport', 'width=device-width, initial-scale=1');
         $pageRenderer->setFavIcon($this->getBackendFavicon($extensionConfiguration, $request));
+        $nonce = $request->getAttribute('nonce');
+        if ($nonce !== null) {
+            $pageRenderer->setNonce($nonce);
+        }
         $this->loadStylesheets($pageRenderer);
     }
 
diff --git a/typo3/sysext/backend/Configuration/ContentSecurityPolicies.php b/typo3/sysext/backend/Configuration/ContentSecurityPolicies.php
new file mode 100644
index 0000000000000000000000000000000000000000..3a57b2ed4be8326789595e27326294e7bfea0f60
--- /dev/null
+++ b/typo3/sysext/backend/Configuration/ContentSecurityPolicies.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Backend;
+
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
+use TYPO3\CMS\Core\Type\Map;
+
+/**
+ * Provides a simple basic Content-Security-Policy for the generic backend scope.
+ */
+return Map::fromEntries([
+    Scope::backend(),
+    new MutationCollection(
+        new Mutation(MutationMode::Extend, Directive::DefaultSrc, SourceKeyword::self),
+        // script-src 'nonce-...' required for importmaps
+        new Mutation(MutationMode::Extend, Directive::ScriptSrc, SourceKeyword::nonceProxy),
+        // `style-src 'unsafe-inline'` required for lit in safari and firefox to allow inline <style> tags
+        // (for browsers that do not support https://caniuse.com/mdn-api_shadowroot_adoptedstylesheets)
+        new Mutation(MutationMode::Extend, Directive::StyleSrc, SourceKeyword::unsafeInline),
+        // `style-src-attr 'unsafe-inline'` required for remaining inline styles, which is okay for color & dimension
+        // (e.g. `<div style="color: #000">` - but NOT having the possibility to use any other assets/files/URIs)
+        new Mutation(MutationMode::Set, Directive::StyleSrcAttr, SourceKeyword::unsafeInline),
+        // allow `data:` images
+        new Mutation(MutationMode::Extend, Directive::ImgSrc, SourceScheme::data),
+        // muuri.js is creating workers from `blob:` (?!?)
+        new Mutation(MutationMode::Set, Directive::WorkerSrc, SourceKeyword::self, SourceScheme::blob),
+        // `frame-src blob:` required for es-module-shims blob: URLs
+        new Mutation(MutationMode::Extend, Directive::FrameSrc, SourceScheme::blob),
+    ),
+]);
diff --git a/typo3/sysext/backend/Configuration/RequestMiddlewares.php b/typo3/sysext/backend/Configuration/RequestMiddlewares.php
index 86b5a6d1a26e8f177fe9c578627af28938b94235..b2d44396c7359e43c98585cf47fd3586890bfea9 100644
--- a/typo3/sysext/backend/Configuration/RequestMiddlewares.php
+++ b/typo3/sysext/backend/Configuration/RequestMiddlewares.php
@@ -78,10 +78,17 @@ return [
             ],
         ],
         /** internal: do not use or reference this middleware in your own code */
+        'typo3/cms-backend/csp-headers' => [
+            'target' => \TYPO3\CMS\Backend\Middleware\ContentSecurityPolicyHeaders::class,
+            'after' => [
+                'typo3/cms-backend/output-compression',
+            ],
+        ],
+        /** internal: do not use or reference this middleware in your own code */
         'typo3/cms-backend/response-headers' => [
             'target' => \TYPO3\CMS\Backend\Middleware\AdditionalResponseHeaders::class,
             'after' => [
-                'typo3/cms-backend/output-compression',
+                'typo3/cms-backend/csp-headers',
             ],
         ],
         /** internal: do not use or reference this middleware in your own code */
diff --git a/typo3/sysext/core/Classes/Configuration/SiteConfiguration.php b/typo3/sysext/core/Classes/Configuration/SiteConfiguration.php
index 4513b2d57a12e331ae289b05812254cba2c48229..3e7733a62eae394637624f691a731bf243f3d76d 100644
--- a/typo3/sysext/core/Classes/Configuration/SiteConfiguration.php
+++ b/typo3/sysext/core/Classes/Configuration/SiteConfiguration.php
@@ -47,18 +47,14 @@ class SiteConfiguration implements SingletonInterface
 {
     protected PhpFrontend $cache;
 
-    /**
-     * @var string
-     */
-    protected $configPath;
+    protected string $configPath;
 
     /**
      * Config yaml file name.
      *
      * @internal
-     * @var string
      */
-    protected $configFileName = 'config.yaml';
+    protected string $configFileName = 'config.yaml';
 
     /**
      * YAML file name with all settings.
@@ -67,13 +63,19 @@ class SiteConfiguration implements SingletonInterface
      */
     protected string $settingsFileName = 'settings.yaml';
 
+    /**
+     * YAML file name with all settings related to Content-Security-Policies.
+     *
+     * @internal
+     */
+    protected string $contentSecurityFileName = 'csp.yaml';
+
     /**
      * Identifier to store all configuration data in cache_core cache.
      *
      * @internal
-     * @var string
      */
-    protected $cacheIdentifier = 'sites-configuration';
+    protected string $cacheIdentifier = 'sites-configuration';
 
     /**
      * Cache stores all configuration as Site objects, as long as they haven't been changed.
@@ -150,9 +152,11 @@ class SiteConfiguration implements SingletonInterface
             // cast $identifier to string, as the identifier can potentially only consist of (int) digit numbers
             $identifier = (string)$identifier;
             $siteSettings = $this->getSiteSettings($identifier, $configuration);
+            $configuration['contentSecurityPolicies'] = $this->getContentSecurityPolicies($identifier);
+
             $rootPageId = (int)($configuration['rootPageId'] ?? 0);
             if ($rootPageId > 0) {
-                $sites[$identifier] = GeneralUtility::makeInstance(Site::class, $identifier, $rootPageId, $configuration, $siteSettings);
+                $sites[$identifier] = new Site($identifier, $rootPageId, $configuration, $siteSettings);
             }
         }
         $this->firstLevelCache = $sites;
@@ -245,6 +249,16 @@ class SiteConfiguration implements SingletonInterface
         return new SiteSettings($settings);
     }
 
+    protected function getContentSecurityPolicies(string $siteIdentifier): array
+    {
+        $fileName = $this->configPath . '/' . $siteIdentifier . '/' . $this->contentSecurityFileName;
+        if (file_exists($fileName)) {
+            $loader = GeneralUtility::makeInstance(YamlFileLoader::class);
+            return $loader->load(GeneralUtility::fixWindowsFilePath($fileName), YamlFileLoader::PROCESS_IMPORTS);
+        }
+        return [];
+    }
+
     public function writeSettings(string $siteIdentifier, array $settings): void
     {
         $fileName = $this->configPath . '/' . $siteIdentifier . '/' . $this->settingsFileName;
diff --git a/typo3/sysext/core/Classes/Core/RequestId.php b/typo3/sysext/core/Classes/Core/RequestId.php
index 2b56d0839e555cf70768df8d3e3da7d1b125d0fe..f7b428b1addea97d42e4dc960c3cffeb90bc0d5c 100644
--- a/typo3/sysext/core/Classes/Core/RequestId.php
+++ b/typo3/sysext/core/Classes/Core/RequestId.php
@@ -17,22 +17,28 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Core;
 
-use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Core\Security\Nonce;
 
 /**
  * @internal
  */
 final class RequestId
 {
-    private string $requestId;
+    public readonly string $long;
+    public readonly string $short;
+    public readonly int $microtime;
+    public readonly Nonce $nonce;
 
     public function __construct()
     {
-        $this->requestId = substr(md5(StringUtility::getUniqueId()), 0, 13);
+        $this->long = bin2hex(random_bytes(20));
+        $this->short = substr($this->long, 0, 13);
+        $this->microtime = (int)(microtime(true) * 1000000);
+        $this->nonce = Nonce::create();
     }
 
     public function __toString(): string
     {
-        return $this->requestId;
+        return $this->short;
     }
 }
diff --git a/typo3/sysext/core/Classes/Http/Security/ReferrerEnforcer.php b/typo3/sysext/core/Classes/Http/Security/ReferrerEnforcer.php
index 363e83bf2ff00c364e6f9a109167206a5efdfef9..ee2cde27d5920ea19e3b1780ac08dcdce6b161c2 100644
--- a/typo3/sysext/core/Classes/Http/Security/ReferrerEnforcer.php
+++ b/typo3/sysext/core/Classes/Http/Security/ReferrerEnforcer.php
@@ -21,6 +21,7 @@ use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Http\HtmlResponse;
 use TYPO3\CMS\Core\Http\NormalizedParams;
+use TYPO3\CMS\Core\Security\Nonce;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 
@@ -64,6 +65,7 @@ class ReferrerEnforcer
         }
         $flags = $options['flags'] ?? [];
         $expiration = $options['expiration'] ?? 5;
+        $nonce = $this->request->getAttribute('nonce');
         // referrer is missing and route requested to refresh
         // (created HTML refresh to enforce having referrer)
         if (($this->request->getQueryParams()['referrer-refresh'] ?? 0) <= time()
@@ -82,16 +84,20 @@ class ReferrerEnforcer
             $scriptUri = $this->resolveAbsoluteWebPath(
                 'EXT:core/Resources/Public/JavaScript/ReferrerRefresh.js'
             );
+            $attributes = ['src' => $scriptUri];
+            if ($nonce instanceof Nonce) {
+                $attributes['nonce'] = $nonce->b64;
+            }
             // simulating navigate event by clicking anchor link
             // since meta-refresh won't change `document.referrer` in e.g. Firefox
             return new HtmlResponse(sprintf(
                 '<html>'
                 . '<head><link rel="icon" href="data:image/svg+xml,"></head>'
-                . '<body><a href="%1$s" id="referrer-refresh">&nbsp;</a>'
-                . '<script src="%2$s"></script></body>'
+                . '<body><a href="%s" id="referrer-refresh">&nbsp;</a>'
+                . '<script %s></script></body>'
                 . '</html>',
                 htmlspecialchars((string)$refreshUri),
-                htmlspecialchars($scriptUri)
+                GeneralUtility::implodeAttributes($attributes, true)
             ));
         }
         $subject = $options['subject'] ?? '';
diff --git a/typo3/sysext/core/Classes/Package/AbstractServiceProvider.php b/typo3/sysext/core/Classes/Package/AbstractServiceProvider.php
index ca2effb8b5dee44b65d71eedaf460575986e9c6a..c9a9f8b1ff980cad7660e2c029b09e85c958f982 100644
--- a/typo3/sysext/core/Classes/Package/AbstractServiceProvider.php
+++ b/typo3/sysext/core/Classes/Package/AbstractServiceProvider.php
@@ -22,6 +22,11 @@ use Psr\Container\ContainerInterface;
 use Psr\Log\LoggerAwareInterface;
 use TYPO3\CMS\Core\DependencyInjection\ServiceProviderInterface;
 use TYPO3\CMS\Core\Log\LogManager;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationOrigin;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationOriginType;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
+use TYPO3\CMS\Core\Type\Map;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
@@ -53,6 +58,7 @@ abstract class AbstractServiceProvider implements ServiceProviderInterface
             // @deprecated since v12, will be removed with v13 together with class PageTsConfigLoader.
             'globalPageTsConfig' => [ static::class, 'configureGlobalPageTsConfig' ],
             'backend.modules' => [ static::class, 'configureBackendModules' ],
+            'content.security.policies' => [static::class, 'configureContentSecurityPolicies'],
             'icons' => [ static::class, 'configureIcons' ],
         ];
     }
@@ -155,6 +161,29 @@ abstract class AbstractServiceProvider implements ServiceProviderInterface
         return $modules;
     }
 
+    /**
+     * @param Map<Scope, Map<MutationOrigin, MutationCollection>> $mutations
+     * @return Map<Scope, Map<MutationOrigin, MutationCollection>>
+     */
+    public static function configureContentSecurityPolicies(ContainerInterface $container, Map $mutations, string $path = null, string $packageName = null): Map
+    {
+        $path = $path ?? static::getPackagePath();
+        $packageName = $packageName ?? static::getPackageName();
+        $fileName = $path . 'Configuration/ContentSecurityPolicies.php';
+        if (file_exists($fileName)) {
+            /** @var Map<Scope, MutationCollection> $mutationsInPackage */
+            $mutationsInPackage = require $fileName;
+            foreach ($mutationsInPackage as $scope => $mutation) {
+                if (!isset($mutations[$scope])) {
+                    $mutations[$scope] = new Map();
+                }
+                $origin = new MutationOrigin(MutationOriginType::package, $packageName);
+                $mutations[$scope][$origin] = $mutation;
+            }
+        }
+        return $mutations;
+    }
+
     public static function configureIcons(ContainerInterface $container, ArrayObject $icons, string $path = null): ArrayObject
     {
         $path = $path ?? static::getPackagePath();
diff --git a/typo3/sysext/core/Classes/Page/ImportMap.php b/typo3/sysext/core/Classes/Page/ImportMap.php
index 28a5a5059d7268bf304dfe06b6ab6ff52fca6d39..47ed7319051ffcba687d4562b83fb715c277a108 100644
--- a/typo3/sysext/core/Classes/Page/ImportMap.php
+++ b/typo3/sysext/core/Classes/Page/ImportMap.php
@@ -114,7 +114,7 @@ class ImportMap
 
     public function render(
         string $urlPrefix,
-        string $nonce,
+        ?string $nonce,
         bool $includePolyfill = true
     ): string {
         if (count($this->extensionsToLoad) === 0 || count($this->getImportMaps()) === 0) {
@@ -128,7 +128,8 @@ class ImportMap
             $importMap,
             JSON_FORCE_OBJECT | JSON_UNESCAPED_SLASHES | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_THROW_ON_ERROR
         );
-        $html[] = sprintf('<script nonce="%s" type="importmap">%s</script>', $nonce, $json);
+        $nonceAttr = $nonce !== null ? ' nonce="' . htmlspecialchars($nonce) . '"' : '';
+        $html[] = sprintf('<script type="importmap"%s>%s</script>', $nonceAttr, $json);
 
         if ($includePolyfill) {
             $importmapPolyfill = $urlPrefix . PathUtility::getPublicResourceWebPath(
@@ -137,8 +138,9 @@ class ImportMap
             );
 
             $html[] = sprintf(
-                '<script src="%s"></script>',
-                htmlspecialchars($importmapPolyfill)
+                '<script src="%s"%s></script>',
+                htmlspecialchars($importmapPolyfill),
+                $nonceAttr
             );
         }
 
diff --git a/typo3/sysext/core/Classes/Page/JavaScriptRenderer.php b/typo3/sysext/core/Classes/Page/JavaScriptRenderer.php
index af85896cf34533f20f4fb61a9ae098c68bf36aab..b19ec26c3551423f9f94d2f62a192e2f6d15bb81 100644
--- a/typo3/sysext/core/Classes/Page/JavaScriptRenderer.php
+++ b/typo3/sysext/core/Classes/Page/JavaScriptRenderer.php
@@ -27,7 +27,7 @@ class JavaScriptRenderer
     protected ImportMap $importMap;
     protected int $javaScriptModuleInstructionFlags = 0;
 
-    public static function create(string $uri = null): self
+    public static function create(?string $uri = null): self
     {
         $uri ??= PathUtility::getAbsoluteWebPath(
             GeneralUtility::getFileAbsFileName('EXT:core/Resources/Public/JavaScript/java-script-item-handler.js')
@@ -107,18 +107,25 @@ class JavaScriptRenderer
         return $this->items->toArray();
     }
 
-    public function render(): string
+    public function render(?string $nonce = null): string
     {
         if ($this->isEmpty()) {
             return '';
         }
-        return $this->createScriptElement([
+        $attributes = [
             'src' => $this->handlerUri,
             'async' => 'async',
-        ], $this->jsonEncode($this->toArray()));
+        ];
+        if ($nonce !== null) {
+            $attributes['nonce'] = $nonce;
+        }
+        return $this->createScriptElement(
+            $attributes,
+            $this->jsonEncode($this->toArray())
+        );
     }
 
-    public function renderImportMap(string $sitePath, string $nonce): string
+    public function renderImportMap(string $sitePath, ?string $nonce = null): string
     {
         if (!$this->isEmpty()) {
             $this->importMap->includeImportsFor('@typo3/core/java-script-item-handler.js');
diff --git a/typo3/sysext/core/Classes/Page/PageRenderer.php b/typo3/sysext/core/Classes/Page/PageRenderer.php
index 6c78c115fd69f5d84c6238aa4323a387700635d3..70b708bfc1a90af2350b617a0363d1380a21aad3 100644
--- a/typo3/sysext/core/Classes/Page/PageRenderer.php
+++ b/typo3/sysext/core/Classes/Page/PageRenderer.php
@@ -32,6 +32,7 @@ use TYPO3\CMS\Core\Package\PackageInterface;
 use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Core\Resource\RelativeCssPathFixer;
 use TYPO3\CMS\Core\Resource\ResourceCompressor;
+use TYPO3\CMS\Core\Security\Nonce;
 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Type\DocType;
@@ -327,6 +328,7 @@ class PageRenderer implements SingletonInterface
     protected $endingSlash = '';
 
     protected JavaScriptRenderer $javaScriptRenderer;
+    protected ?Nonce $nonce = null;
     protected DocType $docType = DocType::html5;
 
     public function __construct(
@@ -362,6 +364,7 @@ class PageRenderer implements SingletonInterface
                 case 'languageServiceFactory':
                 case 'responseFactory':
                 case 'streamFactory':
+                case 'nonce':
                     break;
                 case 'metaTagRegistry':
                     $this->metaTagRegistry->updateState($value);
@@ -393,6 +396,7 @@ class PageRenderer implements SingletonInterface
                 case 'languageServiceFactory':
                 case 'responseFactory':
                 case 'streamFactory':
+                case 'nonce':
                     break;
                 case 'metaTagRegistry':
                     $state[$var] = $this->metaTagRegistry->getState();
@@ -832,6 +836,11 @@ class PageRenderer implements SingletonInterface
         return $this->renderXhtml;
     }
 
+    public function setNonce(Nonce $nonce): void
+    {
+        $this->nonce = $nonce;
+    }
+
     public function setDocType(DocType $docType): void
     {
         $this->docType = $docType;
@@ -2109,9 +2118,7 @@ class PageRenderer implements SingletonInterface
         $out .= $this->javaScriptRenderer->renderImportMap(
             // @todo hookup with PSR-7 request/response and
             GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'),
-            // @todo add CSP Management API for nonces
-            // (currently static for preparatory assertions in Acceptance Testing)
-            'rAnd0m'
+            $this->nonce?->b64
         );
 
         // Include RequireJS
@@ -2146,7 +2153,7 @@ class PageRenderer implements SingletonInterface
                 );
             }
         }
-        $out .= $this->javaScriptRenderer->render();
+        $out .= $this->javaScriptRenderer->render($this->nonce?->b64);
         return $out;
     }
 
@@ -2298,6 +2305,9 @@ class PageRenderer implements SingletonInterface
             if ($properties['title'] ?? false) {
                 $tagAttributes['title'] = $properties['title'];
             }
+            if ($properties['nonce'] ?? $this->nonce?->b64) {
+                $tagAttributes['nonce'] = $properties['nonce'] ?? $this->nonce?->b64;
+            }
             $tagAttributes = array_merge($tagAttributes, $properties['tagAttributes'] ?? []);
             $tag = '<link ' . GeneralUtility::implodeAttributes($tagAttributes, true, true) . $this->endingSlash . '>';
         }
@@ -2363,6 +2373,9 @@ class PageRenderer implements SingletonInterface
                 if ($properties['crossorigin'] ?? false) {
                     $tagAttributes['crossorigin'] = $properties['crossorigin'];
                 }
+                if ($properties['nonce'] ?? $this->nonce?->b64) {
+                    $tagAttributes['nonce'] = $properties['nonce'] ?? $this->nonce?->b64;
+                }
                 $tagAttributes = array_merge($tagAttributes, $properties['tagAttributes'] ?? []);
                 $tag = '<script ' . GeneralUtility::implodeAttributes($tagAttributes, true, true) . '></script>';
                 if ($properties['allWrap'] ?? false) {
@@ -2421,6 +2434,9 @@ class PageRenderer implements SingletonInterface
                 if ($properties['crossorigin'] ?? false) {
                     $tagAttributes['crossorigin'] = $properties['crossorigin'];
                 }
+                if ($properties['nonce'] ?? $this->nonce?->b64) {
+                    $tagAttributes['nonce'] = $properties['nonce'] ?? $this->nonce?->b64;
+                }
                 $tagAttributes = array_merge($tagAttributes, $properties['tagAttributes'] ?? []);
                 $tag = '<script ' . GeneralUtility::implodeAttributes($tagAttributes, true, true) . '></script>';
                 if ($properties['allWrap'] ?? false) {
@@ -2449,7 +2465,7 @@ class PageRenderer implements SingletonInterface
     }
 
     /**
-     * Render inline JavaScript
+     * Render inline JavaScript (must not apply `nonce="..."` if defined).
      *
      * @return array|string[] jsInline and jsFooterInline string
      */
@@ -2858,6 +2874,9 @@ class PageRenderer implements SingletonInterface
         if ($properties['title'] ?? false) {
             $tagAttributes['title'] = $properties['title'];
         }
+        if ($properties['nonce'] ?? $this->nonce?->b64) {
+            $tagAttributes['nonce'] = $properties['nonce'] ?? $this->nonce?->b64;
+        }
         $tagAttributes = array_merge($tagAttributes, $properties['tagAttributes'] ?? []);
         return '<style ' . GeneralUtility::implodeAttributes($tagAttributes, true, true) . '>' . LF
             . '/*<![CDATA[*/' . LF . '<!-- ' . LF
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Directive.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Directive.php
new file mode 100644
index 0000000000000000000000000000000000000000..4584b6ae4b6619e453af08231ffbf86776e63e53
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Directive.php
@@ -0,0 +1,102 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+/**
+ * Representation of Content-Security-Policy directives
+ * see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives
+ */
+enum Directive: string
+{
+    case DefaultSrc = 'default-src';
+    case BaseUri = 'base-uri';
+    case ChildSrc = 'child-src';
+    case ConnectSrc = 'connect-src';
+    case FontSrc = 'font-src';
+    case FormAction = 'form-action';
+    case FrameAncestors = 'frame-ancestors';
+    case FrameSrc = 'frame-src';
+    case ImgSrc = 'img-src';
+    case ManifestSrc = 'manifest-src';
+    case MediaSrc = 'media-src';
+    case ObjectSrc = 'object-src';
+    // @deprecated (used for Safari, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/plugin-types)
+    case PluginTypes = 'plugin-types';
+    // @deprecated (see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri)
+    case ReportUri = 'report-uri';
+    case Sandbox = 'sandbox';
+    case ScriptSrc = 'script-src';
+    case ScriptSrcAttr = 'script-src-attr';
+    case ScriptSrcElem = 'script-src-elem';
+    case StrictDynamic = 'strict-dynamic';
+    case StyleSrc = 'style-src';
+    case StyleSrcAttr = 'style-src-attr';
+    case StyleSrcElem = 'style-src-elem';
+    case UnsafeHashes = 'unsafe-hashes';
+    case UpgradeInsecureRequests = 'upgrade-insecure-requests';
+    case WorkerSrc = 'worker-src';
+
+    /**
+     * @return list<self>
+     */
+    public function getAncestors(): array
+    {
+        $ancestors = self::ancestorMap()[$this] ?? [];
+        if ($this !== self::DefaultSrc) {
+            $ancestors[] = self::DefaultSrc;
+        }
+        return $ancestors;
+    }
+
+    public function isReasonable(): bool
+    {
+        return !in_array($this, self::reasonableItems(), true);
+    }
+
+    /**
+     * @return \WeakMap<self, list<self>>
+     */
+    private static function ancestorMap(): \WeakMap
+    {
+        /** @var \WeakMap<self, list<self>> $map temporary, internal \WeakMap */
+        $map = new \WeakMap();
+        $map[self::ScriptSrcAttr] = [self::ScriptSrc];
+        $map[self::ScriptSrcElem] = [self::ScriptSrc];
+        $map[self::StyleSrcAttr] = [self::StyleSrc];
+        $map[self::StyleSrcElem] = [self::StyleSrc];
+        $map[self::FrameSrc] = [self::ChildSrc];
+        $map[self::WorkerSrc] = [self::ChildSrc, self::ScriptSrc];
+        return $map;
+    }
+
+    /**
+     * @return list<self>
+     */
+    private static function reasonableItems(): array
+    {
+        return [
+            self::ConnectSrc,
+            self::FontSrc,
+            self::FrameSrc,
+            self::ImgSrc,
+            self::MediaSrc,
+            self::ScriptSrcElem,
+            self::StyleSrcElem,
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Event/PolicyMutatedEvent.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Event/PolicyMutatedEvent.php
new file mode 100644
index 0000000000000000000000000000000000000000..a54474117001a88cbf3b7192a456738f1152fb93
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Event/PolicyMutatedEvent.php
@@ -0,0 +1,76 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event;
+
+use Psr\EventDispatcher\StoppableEventInterface;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
+
+final class PolicyMutatedEvent implements StoppableEventInterface
+{
+    private bool $stopPropagation = false;
+    private Policy $currentPolicy;
+    /**
+     * @var list<MutationCollection>
+     */
+    private array $mutationCollections;
+
+    public function __construct(
+        public readonly Scope $scope,
+        public readonly Policy $defaultPolicy,
+        Policy $currentPolicy,
+        MutationCollection ...$mutationCollections
+    ) {
+        $this->currentPolicy = $currentPolicy;
+        $this->mutationCollections = $mutationCollections;
+    }
+
+    public function isPropagationStopped(): bool
+    {
+        return $this->stopPropagation;
+    }
+
+    public function stopPropagation(): void
+    {
+        $this->stopPropagation = true;
+    }
+
+    public function getCurrentPolicy(): Policy
+    {
+        return $this->currentPolicy;
+    }
+
+    public function setCurrentPolicy(Policy $currentPolicy): void
+    {
+        $this->currentPolicy = $currentPolicy;
+    }
+
+    /**
+     * @return list<MutationCollection>
+     */
+    public function getMutationCollections(): array
+    {
+        return $this->mutationCollections;
+    }
+
+    public function setMutationCollections(MutationCollection ...$mutationCollections): void
+    {
+        $this->mutationCollections = $mutationCollections;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/ModelService.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/ModelService.php
new file mode 100644
index 0000000000000000000000000000000000000000..875fb8dc8335bf9750924550b820eaed2a1b9378
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/ModelService.php
@@ -0,0 +1,117 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+use TYPO3\CMS\Core\Security\Nonce;
+
+/**
+ * Helpers for working with Content-Security-Policy models.
+ *
+ * @internal
+ */
+class ModelService
+{
+    public function buildMutationFromArray(array $array): Mutation
+    {
+        return new Mutation(
+            MutationMode::tryFrom($array['mode'] ?? ''),
+            Directive::tryFrom($array['directive'] ?? ''),
+            ...$this->buildSourcesFromItems(...($array['sources'] ?? []))
+        );
+    }
+
+    public function buildSourcesFromItems(string ...$items): array
+    {
+        $sources = [];
+        foreach ($items as $item) {
+            $source = $this->buildSourceFromString($item);
+            if ($source === null) {
+                throw new \InvalidArgumentException(
+                    sprintf('Could not convert source item "%s"', $item),
+                    1677261214
+                );
+            }
+            $sources[] = $source;
+        }
+        return $sources;
+    }
+
+    public function buildSourceFromString(string $string): null|SourceKeyword|SourceScheme|UriValue|RawValue
+    {
+        if (str_starts_with($string, "'nonce-") && $string[-1] === "'") {
+            // use a proxy instead of a real Nonce instance
+            return SourceKeyword::nonceProxy;
+        }
+        if ($string[0] === "'" && $string[-1] === "'") {
+            return SourceKeyword::tryFrom(substr($string, 1, -1));
+        }
+        if ($string[-1] === ':') {
+            return SourceScheme::tryFrom(substr($string, 0, -1));
+        }
+        try {
+            return new UriValue($string);
+        } catch (\InvalidArgumentException) {
+            // no handling here
+        }
+        return new RawValue($string);
+    }
+
+    public function serializeSources(SourceKeyword|SourceScheme|Nonce|UriValue|RawValue ...$sources): array
+    {
+        $serialized = [];
+        foreach ($sources as $source) {
+            if ($source instanceof SourceKeyword && $source->vetoes()) {
+                $serialized = [];
+            }
+            $serialized[] = $this->serializeSource($source);
+        }
+        return $serialized;
+    }
+
+    public function compileSources(Nonce $nonce, SourceCollection $collection): array
+    {
+        $compiled = [];
+        foreach ($collection->sources as $source) {
+            if ($source instanceof SourceKeyword && $source->vetoes()) {
+                $compiled = [];
+            }
+            $compiled[] = $this->serializeSource($source, $nonce);
+        }
+        return $compiled;
+    }
+
+    /**
+     * @param Nonce|null $nonce used to substitute `SourceKeyword::nonceProxy` items during compilation
+     */
+    public function serializeSource(SourceKeyword|SourceScheme|Nonce|UriValue|RawValue $source, Nonce $nonce = null): string
+    {
+        if ($source instanceof Nonce) {
+            return "'nonce-" . $source->b64 . "'";
+        }
+        if ($source === SourceKeyword::nonceProxy && $nonce !== null) {
+            return "'nonce-" . $nonce->b64 . "'";
+        }
+        if ($source instanceof SourceKeyword) {
+            return "'" . $source->value . "'";
+        }
+        if ($source instanceof SourceScheme) {
+            return $source->value . ':';
+        }
+        return (string)$source;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Mutation.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Mutation.php
new file mode 100644
index 0000000000000000000000000000000000000000..933d13e43ec67d6e9e0ac6c7b48214928dc3f921
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Mutation.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+use TYPO3\CMS\Core\Security\Nonce;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Representation of a Content-Security-Policy mutation, changing an existing policy directive.
+ */
+class Mutation implements \JsonSerializable
+{
+    /**
+     * @var list<SourceKeyword|SourceScheme|Nonce|UriValue|RawValue>
+     */
+    public readonly array $sources;
+
+    public function __construct(
+        public readonly MutationMode $mode,
+        public readonly Directive $directive,
+        SourceKeyword|SourceScheme|Nonce|UriValue|RawValue ...$sources
+    ) {
+        if ($sources !== [] && $this->mode === MutationMode::Remove) {
+            throw new \LogicException(
+                'Cannot remove and declare sources at the same time',
+                1677244893
+            );
+        }
+        $this->sources = $sources;
+    }
+
+    public function jsonSerialize(): array
+    {
+        $service = GeneralUtility::makeInstance(ModelService::class);
+        return [
+            'mode' => $this->mode,
+            'directive' => $this->directive,
+            'sources' => $service->serializeSources(...$this->sources),
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationCollection.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationCollection.php
new file mode 100644
index 0000000000000000000000000000000000000000..26f8fe0acda1e97269b2a2b8cb74952d698d0e8b
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationCollection.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+/**
+ * A collection of mutations (sic!).
+ */
+final class MutationCollection implements \JsonSerializable
+{
+    /**
+     * @var list<Mutation>
+     */
+    public readonly array $mutations;
+
+    public function __construct(Mutation ...$mutations)
+    {
+        $this->mutations = $mutations;
+    }
+
+    public function jsonSerialize(): array
+    {
+        return [
+            'mutations' => $this->mutations,
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationMode.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationMode.php
new file mode 100644
index 0000000000000000000000000000000000000000..6c3595a759f552ef390d5b9f4f1d2f56055190a8
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationMode.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+/**
+ * The mode used in mutations (sic!).
+ */
+enum MutationMode: string
+{
+    case Set = 'set';
+    case Extend = 'extend';
+    case Remove = 'remove';
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationOrigin.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationOrigin.php
new file mode 100644
index 0000000000000000000000000000000000000000..86b22632e52d860bbdb08e4c35fbc52a8d8f65f9
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationOrigin.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+/**
+ * Representation of a mutation origin, to keep track of resolutions
+ * to the Content-Security-Policy and how to revert again later.
+ */
+class MutationOrigin
+{
+    public function __construct(
+        public readonly MutationOriginType $type,
+        public readonly string $value
+    ) {
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationOriginType.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationOriginType.php
new file mode 100644
index 0000000000000000000000000000000000000000..bfdc1eface9064260d45e45d14e9360f5c45bacc
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationOriginType.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+enum MutationOriginType: string
+{
+    case package = 'package';
+    case resolution = 'resolution';
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Policy.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Policy.php
new file mode 100644
index 0000000000000000000000000000000000000000..08da78073ebd9b560d293ff10af2b1c550221966
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Policy.php
@@ -0,0 +1,233 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+use TYPO3\CMS\Core\Security\Nonce;
+use TYPO3\CMS\Core\Type\Map;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Representation of the whole Content-Security-Policy
+ * see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
+ *
+ * @internal This implementation still might be adjusted
+ */
+class Policy implements \Stringable
+{
+    /**
+     * @var Map<Directive, SourceCollection>
+     */
+    protected Map $directives;
+
+    /**
+     * @param Nonce $nonce used to substitute `SourceKeyword::nonceProxy` items during compilation
+     * @param SourceCollection|SourceKeyword|SourceScheme|Nonce|UriValue|RawValue ...$sources (optional) default-src sources
+     */
+    public function __construct(
+        protected readonly Nonce $nonce,
+        SourceCollection|SourceKeyword|SourceScheme|Nonce|UriValue|RawValue ...$sources
+    ) {
+        $this->directives = new Map();
+        $collection = $this->asMergedSourceCollection(...$sources);
+        if (!$collection->isEmpty()) {
+            $this->directives[Directive::DefaultSrc] = $collection;
+        }
+    }
+
+    public function __toString(): string
+    {
+        return $this->compile();
+    }
+
+    public function isEmpty(): bool
+    {
+        return count($this->directives) === 0;
+    }
+
+    /**
+     * Applies mutations/changes to the current policy.
+     */
+    public function mutate(MutationCollection|Mutation ...$mutations): self
+    {
+        $self = $this;
+        foreach ($mutations as $mutation) {
+            if ($mutation instanceof MutationCollection) {
+                $self = $self->mutate(...$mutation->mutations);
+            } elseif ($mutation->mode === MutationMode::Set) {
+                $self = $self->set($mutation->directive, ...$mutation->sources);
+            } elseif ($mutation->mode === MutationMode::Extend) {
+                $self = $self->extend($mutation->directive, ...$mutation->sources);
+            } elseif ($mutation->mode === MutationMode::Remove) {
+                $self = $self->remove($mutation->directive);
+            }
+        }
+        return $self;
+    }
+
+    /**
+     * Sets (overrides) the 'default-src' directive, which is also the fall-back for other more specific directives.
+     */
+    public function default(SourceCollection|SourceKeyword|SourceScheme|Nonce|UriValue|RawValue ...$sources): self
+    {
+        return $this->set(Directive::DefaultSrc, ...$sources);
+    }
+
+    /**
+     * Extends a specific directive, either by appending sources or by inheriting from an ancestor directive.
+     */
+    public function extend(
+        Directive $directive,
+        SourceCollection|SourceKeyword|SourceScheme|Nonce|UriValue|RawValue ...$sources
+    ): self {
+        $collection = $this->asMergedSourceCollection(...$sources);
+        if ($collection->isEmpty()) {
+            return $this;
+        }
+        foreach ($directive->getAncestors() as $ancestorDirective) {
+            if (isset($this->directives[$ancestorDirective])) {
+                $ancestorCollection = $this->directives[$ancestorDirective];
+                break;
+            }
+        }
+        $targetCollection = $this->asMergedSourceCollection(...array_filter([
+            $ancestorCollection ?? null,
+            $this->directives[$directive] ?? null,
+            $collection,
+        ]));
+        return $this->set($directive, $targetCollection);
+    }
+
+    /**
+     * Sets (overrides) a specific directive.
+     */
+    public function set(
+        Directive $directive,
+        SourceCollection|SourceKeyword|SourceScheme|Nonce|UriValue|RawValue ...$sources
+    ): self {
+        $collection = $this->asMergedSourceCollection(...$sources);
+        if ($collection->isEmpty()) {
+            return $this;
+        }
+        $target = clone $this;
+        $target->directives[$directive] = $collection;
+        return $target;
+    }
+
+    /**
+     * Removes a specific directive.
+     */
+    public function remove(Directive $directive): self
+    {
+        if (!isset($this->directives[$directive])) {
+            return $this;
+        }
+        $target = clone $this;
+        unset($target->directives[$directive]);
+        return $target;
+    }
+
+    /**
+     * Sets the 'report-uri' directive and appends 'report-sample' to existing & applicable directives.
+     */
+    public function report(UriValue $reportUri): self
+    {
+        $target = $this->set(Directive::ReportUri, $reportUri);
+        $reportSample = SourceKeyword::reportSample;
+        foreach ($target->directives as $directive => $collection) {
+            if ($reportSample->isApplicable($directive)) {
+                $target->directives[$directive] = $collection->with($reportSample);
+            }
+        }
+        return $target;
+    }
+
+    /**
+     * Prepares the policy for finally being serialized and issued as HTTP header.
+     * This step aims to optimize several combinations, or adjusts directives when 'strict-dynamic' is used.
+     */
+    public function prepare(): self
+    {
+        $purged = false;
+        $directives = clone $this->directives;
+        $comparator = [$this, 'compareSources'];
+        foreach ($directives as $directive => $collection) {
+            foreach ($directive->getAncestors() as $ancestorDirective) {
+                $ancestorCollection = $directives[$ancestorDirective] ?? null;
+                if ($ancestorCollection !== null
+                    && array_udiff($collection->sources, $ancestorCollection->sources, $comparator) === []
+                    && array_udiff($ancestorCollection->sources, $collection->sources, $comparator) === []
+                ) {
+                    $purged = true;
+                    unset($directives[$directive]);
+                    continue 2;
+                }
+            }
+        }
+        foreach ($directives as $directive => $collection) {
+            // applies implicit changes to sources in case 'strict-dynamic' is used
+            if ($collection->contains(SourceKeyword::strictDynamic)) {
+                $directives[$directive] = SourceKeyword::strictDynamic->applySourceImplications($collection) ?? $collection;
+            }
+        }
+        if (!$purged) {
+            return $this;
+        }
+        $target = clone $this;
+        $target->directives = $directives;
+        return $target;
+    }
+
+    /**
+     * Compiles this policy and returns the serialized representation to be used as HTTP header value.
+     */
+    public function compile(): string
+    {
+        $policyParts = [];
+        $service = GeneralUtility::makeInstance(ModelService::class);
+        foreach ($this->prepare()->directives as $directive => $collection) {
+            $directiveParts = $service->compileSources($this->nonce, $collection);
+            if ($directiveParts !== []) {
+                array_unshift($directiveParts, $directive->value);
+                $policyParts[] = implode(' ', $directiveParts);
+            }
+        }
+        return implode('; ', $policyParts);
+    }
+
+    protected function compareSources(
+        SourceKeyword|SourceScheme|Nonce|UriValue|RawValue $a,
+        SourceKeyword|SourceScheme|Nonce|UriValue|RawValue $b
+    ): int {
+        $service = GeneralUtility::makeInstance(ModelService::class);
+        return $service->serializeSource($a) <=> $service->serializeSource($b);
+    }
+
+    protected function asMergedSourceCollection(SourceCollection|SourceKeyword|SourceScheme|Nonce|UriValue|RawValue ...$subjects): SourceCollection
+    {
+        $collections = array_filter($subjects, static fn ($source) => $source instanceof SourceCollection);
+        $sources = array_filter($subjects, static fn ($source) => !$source instanceof SourceCollection);
+        if ($sources !== []) {
+            $collections[] = new SourceCollection(...$sources);
+        }
+        $target = new SourceCollection();
+        foreach ($collections as $collection) {
+            $target = $target->merge($collection);
+        }
+        return $target;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/PolicyProvider.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/PolicyProvider.php
new file mode 100644
index 0000000000000000000000000000000000000000..cad76e1778bd59c65d42e16e962a31448f2645fb
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/PolicyProvider.php
@@ -0,0 +1,117 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+use Psr\EventDispatcher\EventDispatcherInterface;
+use TYPO3\CMS\Core\Core\RequestId;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyMutatedEvent;
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\SiteFinder;
+use TYPO3\CMS\Core\Type\Map;
+
+/**
+ * Provide a Content-Security-Policy representation for a given scope (e.g. backend, frontend, frontend.my-site).
+ *
+ * @internal
+ */
+final class PolicyProvider
+{
+    /**
+     * @param Map<Scope, Map<MutationOrigin, MutationCollection>> $mutations
+     */
+    public function __construct(
+        private readonly Map $mutations,
+        private readonly RequestId $requestId,
+        private readonly SiteFinder $siteFinder,
+        private readonly ModelService $modelService,
+        private readonly EventDispatcherInterface $eventDispatcher,
+    ) {
+    }
+
+    /**
+     * Provides the complete, dynamically mutated policy to be used in HTTP responses.
+     */
+    public function provideFor(Scope $scope): Policy
+    {
+        // @todo add policy cache per scope
+        $defaultPolicy = $this->provideDefaultFor($scope);
+        $mutationCollections = [];
+        $currentPolicy = $defaultPolicy->mutate(...$mutationCollections);
+        $event = new PolicyMutatedEvent($scope, $defaultPolicy, $currentPolicy, ...$mutationCollections);
+        $this->eventDispatcher->dispatch($event);
+        return $event->getCurrentPolicy();
+    }
+
+    /**
+     * Provides the base policy which contains only static mutations - either from
+     * `Configuration/ContentSecurityPolicies.php of each extension, or from the
+     * corresponding `contentSecurityPolicies.mutations` section of a frontend
+     * site definition.
+     */
+    public function provideDefaultFor(Scope $scope): Policy
+    {
+        // @todo add runtime policy cache per scope
+        $isFrontendSite = $scope->siteIdentifier !== null && $scope->type->isFrontend();
+        if ($isFrontendSite && $this->shallInheritDefault($scope)) {
+            $policy = $this->provideFor(Scope::frontend());
+        } else {
+            $policy = new Policy($this->requestId->nonce);
+        }
+        // apply static mutations (from DI, declared in `Configuration/ContentSecurityPolicies.php`)
+        foreach ($this->mutations[$scope] ?? [] as $mutationCollection) {
+            $policy = $policy->mutate($mutationCollection);
+        }
+        // apply site mutations (declared in corresponding site configuration's csp.yaml file)
+        if ($isFrontendSite) {
+            foreach ($this->resolveSiteMutations($scope) as $mutation) {
+                $policy = $policy->mutate($mutation);
+            }
+        }
+        return $policy;
+    }
+
+    /**
+     * Whether to inherit default (site-unspecific) frontend policy mutations.
+     */
+    private function shallInheritDefault(Scope $scope): bool
+    {
+        $site = $this->resolveSite($scope);
+        return (bool)($site->getConfiguration()['contentSecurityPolicies']['inheritDefault'] ?? true);
+    }
+
+    /**
+     * @return list<Mutation>
+     */
+    private function resolveSiteMutations(Scope $scope): array
+    {
+        $site = $this->resolveSite($scope);
+        $mutationConfigurations = $site->getConfiguration()['contentSecurityPolicies']['mutations'] ?? [];
+        if (empty($mutationConfigurations) || !is_array($mutationConfigurations)) {
+            return [];
+        }
+        return array_map(
+            fn (array $array) => $this->modelService->buildMutationFromArray($array),
+            $mutationConfigurations
+        );
+    }
+
+    private function resolveSite(Scope $scope): Site
+    {
+        return $scope->site ?? $this->siteFinder->getSiteByIdentifier($scope->siteIdentifier);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/RawValue.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/RawValue.php
new file mode 100644
index 0000000000000000000000000000000000000000..216e2dcfa65eaa11e225c3dcb2e7de55f8f6d6af
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/RawValue.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+/**
+ * Representation of a plain, raw string value that does not have
+ * a particular meaning in the terms of Content-Security-Policy.
+ *
+ * @internal Might be changed or removed at a later time
+ */
+class RawValue implements \Stringable
+{
+    public function __construct(public readonly string $value)
+    {
+    }
+
+    public function __toString(): string
+    {
+        return $this->value;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Scope.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Scope.php
new file mode 100644
index 0000000000000000000000000000000000000000..8a840f1c42dbb39cdc49c639d1aabda8b54d163f
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Scope.php
@@ -0,0 +1,121 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+use TYPO3\CMS\Core\Http\ApplicationType;
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\Entity\SiteInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Representation of a specific application type scope (backend, frontend),
+ * which can optionally be enriched by site-related details.
+ */
+final class Scope implements \Stringable, \JsonSerializable
+{
+    /**
+     * @var array<string, self>
+     */
+    private static array $singletons = [];
+
+    public static function backend(): self
+    {
+        return self::asSingleton(new self(ApplicationType::BACKEND));
+    }
+
+    public static function frontend(): self
+    {
+        return self::asSingleton(new self(ApplicationType::FRONTEND));
+    }
+
+    public static function frontendSite(?SiteInterface $site): self
+    {
+        // PHPStan fails, see https://github.com/phpstan/phpstan/issues/8464
+        // @phpstan-ignore-next-line
+        if (!$site instanceof Site || is_subclass_of($site, Site::class)) {
+            return self::frontend();
+        }
+        return self::asSingleton(new self(ApplicationType::FRONTEND, $site->getIdentifier(), $site));
+    }
+
+    public static function frontendSiteIdentifier(string $siteIdentifier): self
+    {
+        return self::asSingleton(new self(ApplicationType::FRONTEND, $siteIdentifier));
+    }
+
+    public static function from(string $value): self
+    {
+        $parts = GeneralUtility::trimExplode('.', $value, true);
+        $type = ApplicationType::tryFrom($parts[0] ?? '');
+        $siteIdentifier = $parts[1] ?? null;
+        if ($type === null) {
+            throw new \LogicException(
+                sprintf('Could not resolve application type from "%s"', $value),
+                1677424928
+            );
+        }
+        return self::asSingleton(new self($type, $siteIdentifier));
+    }
+
+    public static function reset(): void
+    {
+        self::$singletons = [];
+    }
+
+    public static function tryFrom(string $value): ?self
+    {
+        try {
+            return self::from($value);
+        } catch (\LogicException) {
+            return null;
+        }
+    }
+
+    private static function asSingleton(self $self): self
+    {
+        $id = (string)$self;
+        if (!isset(self::$singletons[$id])) {
+            self::$singletons[$id] = $self;
+        }
+        return self::$singletons[$id];
+    }
+
+    /**
+     * Use static functions to create singleton instances.
+     */
+    private function __construct(
+        public readonly ApplicationType $type,
+        public readonly ?string $siteIdentifier = null,
+        public readonly ?Site $site = null,
+    ) {
+    }
+
+    public function __toString(): string
+    {
+        $value = $this->type->value;
+        if ($this->siteIdentifier !== null) {
+            $value .= '.' . $this->siteIdentifier;
+        }
+        return $value;
+    }
+
+    public function jsonSerialize(): string
+    {
+        return (string)$this;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/SourceCollection.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/SourceCollection.php
new file mode 100644
index 0000000000000000000000000000000000000000..368a975d5828aa37a05ccce12f262c6046b1a4da
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/SourceCollection.php
@@ -0,0 +1,126 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+use TYPO3\CMS\Core\Security\Nonce;
+
+/**
+ * A collection of sources (sic!).
+ * @internal This implementation still might be adjusted
+ */
+final class SourceCollection
+{
+    /**
+     * @var list<SourceKeyword|SourceScheme|Nonce|UriValue|RawValue>
+     */
+    public readonly array $sources;
+
+    public function __construct(SourceKeyword|SourceScheme|Nonce|UriValue|RawValue ...$sources)
+    {
+        $this->sources = $sources;
+    }
+
+    public function isEmpty(): bool
+    {
+        return $this->sources === [];
+    }
+
+    public function merge(self $other): self
+    {
+        return $this->with(...$other->sources);
+    }
+
+    public function with(SourceKeyword|SourceScheme|Nonce|UriValue|RawValue ...$subjects): self
+    {
+        $subjects = array_filter(
+            $subjects,
+            fn ($subject) => !in_array($subject, $this->sources, true)
+        );
+        if ($subjects === []) {
+            return $this;
+        }
+        return new self(...array_merge($this->sources, $subjects));
+    }
+
+    public function without(SourceKeyword|SourceScheme|Nonce|UriValue|RawValue ...$subjects): self
+    {
+        $sources = array_filter(
+            $this->sources,
+            fn ($source) => !in_array($source, $subjects, true)
+        );
+        if (count($this->sources) === count($sources)) {
+            return $this;
+        }
+        return new self(...$sources);
+    }
+
+    /**
+     * @param class-string ...$subjectTypes
+     */
+    public function withoutTypes(string ...$subjectTypes): self
+    {
+        $sources = array_filter(
+            $this->sources,
+            fn ($source) => !$this->isSourceOfTypes($source, ...$subjectTypes)
+        );
+        if (count($this->sources) === count($sources)) {
+            return $this;
+        }
+        return new self(...$sources);
+    }
+
+    /**
+     * Determines whether at least one source matches.
+     */
+    public function contains(SourceKeyword|SourceScheme|Nonce|UriValue|RawValue ...$subjects): bool
+    {
+        foreach ($this->sources as $source) {
+            if (in_array($source, $subjects, true)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Determines whether at least one type matches.
+     * @param class-string ...$subjectTypes
+     */
+    public function containsTypes(string ...$subjectTypes): bool
+    {
+        foreach ($this->sources as $source) {
+            if ($this->isSourceOfTypes($source, ...$subjectTypes)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @param class-string ...$types
+     */
+    private function isSourceOfTypes(SourceKeyword|SourceScheme|Nonce|UriValue|RawValue $source, string ...$types): bool
+    {
+        foreach ($types as $type) {
+            if (is_a($source, $type)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/SourceKeyword.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/SourceKeyword.php
new file mode 100644
index 0000000000000000000000000000000000000000..251600ce374377bd8eeb051d9dbfd6782eb73e01
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/SourceKeyword.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+use TYPO3\CMS\Core\Security\Nonce;
+
+/**
+ * Representation of Content-Security-Policy source keywords
+ * see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources#sources
+ */
+enum SourceKeyword: string
+{
+    case none = 'none';
+    case self = 'self';
+    case unsafeInline = 'unsafe-inline';
+    case unsafeEval = 'unsafe-eval';
+    case wasmUnsafeEval = 'wasm-unsafe-eval';
+    case reportSample = 'report-sample';
+    case strictDynamic = 'strict-dynamic';
+    // nonce proxy is substituted when compiling the whole policy
+    // (this value does NOT exist in the CSP definition, it's specific to TYPO3 only)
+    case nonceProxy = 'nonce-proxy';
+
+    public function vetoes(): bool
+    {
+        return $this === self::none;
+    }
+
+    public function isApplicable(Directive $directive): bool
+    {
+        // temporary, internal \WeakMap
+        $onlyApplicableTo = new \WeakMap();
+        $onlyApplicableTo[self::reportSample] = [
+            Directive::ScriptSrc, Directive::ScriptSrcAttr, Directive::ScriptSrcElem,
+            Directive::StyleSrc, Directive::StyleSrcAttr, Directive::StyleSrcElem,
+        ];
+        return !isset($onlyApplicableTo[$this]) || in_array($directive, $onlyApplicableTo[$this], true);
+    }
+
+    public function applySourceImplications(SourceCollection $sources): ?SourceCollection
+    {
+        if ($this !== self::strictDynamic) {
+            return null;
+        }
+        // adjust existing directives when using 'strict-dynamic' (e.g. for Google Maps)
+        // see https://www.w3.org/TR/CSP3/#strict-dynamic-usage
+        $newSources = $sources
+            ->without(SourceKeyword::self, SourceKeyword::unsafeInline)
+            ->withoutTypes(UriValue::class, SourceScheme::class);
+        if (!$sources->contains(self::nonceProxy) && !$sources->containsTypes(Nonce::class)) {
+            $newSources = $newSources->with(self::nonceProxy);
+        }
+        if ($sources === $newSources) {
+            return null;
+        }
+        return $newSources;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/SourceScheme.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/SourceScheme.php
new file mode 100644
index 0000000000000000000000000000000000000000..b188d55cd47bee6836e605f1b93f6cd627379a62
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/SourceScheme.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+/**
+ * Representation of Content-Security-Policy source schemes
+ * see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources#sources
+ */
+enum SourceScheme: string
+{
+    case blob = 'blob';
+    case data = 'data';
+    case http = 'http';
+    case https = 'https';
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/UriValue.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/UriValue.php
new file mode 100644
index 0000000000000000000000000000000000000000..0d23a08e57e5317a3a8db13dfc6534d35b3773d2
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/UriValue.php
@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
+
+use Psr\Http\Message\UriInterface;
+use TYPO3\CMS\Core\Http\Uri;
+
+/**
+ * Bridge to UriInterface to be used in Content-Security-Policy models,
+ * which e.g. supports wildcard domains, like `*.typo3.org` or `https://*.typo3.org`.
+ */
+final class UriValue extends Uri implements \Stringable
+{
+    private string $domainName = '';
+
+    public static function fromUri(UriInterface $other): self
+    {
+        return new self((string)$other);
+    }
+
+    public function __toString(): string
+    {
+        if ($this->domainName !== '') {
+            return $this->domainName;
+        }
+        return parent::__toString();
+    }
+
+    public function getDomainName(): string
+    {
+        return $this->domainName;
+    }
+
+    protected function parseUri($uri): void
+    {
+        parent::parseUri($uri);
+        // ignore fragments per default
+        $this->fragment = '';
+        // handle domain names that were recognized as paths
+        if ($this->canBeParsedAsWildcardDomainName()) {
+            $this->domainName = '*.' . substr($this->path, 4);
+        } elseif ($this->canBeParsedAsDomainName()) {
+            $this->domainName = $this->path;
+        }
+    }
+
+    private function canBeParsedAsDomainName(): bool
+    {
+        return $this->path !== ''
+            && $this->scheme === ''
+            && $this->host === ''
+            && $this->query === ''
+            && $this->userInfo === ''
+            && $this->validateDomainName($this->path);
+    }
+
+    private function canBeParsedAsWildcardDomainName(): bool
+    {
+        if ($this->path === ''
+            || $this->scheme !== ''
+            || $this->host !== ''
+            || $this->query !== ''
+            || $this->userInfo !== ''
+            || stripos($this->path, '%2A') !== 0
+        ) {
+            return false;
+        }
+        $possibleDomainName = substr($this->path, 4);
+        return $this->validateDomainName($possibleDomainName);
+    }
+
+    private function validateDomainName(string $value): bool
+    {
+        return filter_var($value, FILTER_VALIDATE_DOMAIN) !== false;
+    }
+}
diff --git a/typo3/sysext/core/Classes/ServiceProvider.php b/typo3/sysext/core/Classes/ServiceProvider.php
index 1fe8de2cb2e2a529da20d4e464b65a00ca0d5e8b..09ca60852b34501193317655478246dd8df95869 100644
--- a/typo3/sysext/core/Classes/ServiceProvider.php
+++ b/typo3/sysext/core/Classes/ServiceProvider.php
@@ -30,6 +30,7 @@ use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
 use TYPO3\CMS\Core\Imaging\IconRegistry;
 use TYPO3\CMS\Core\Package\AbstractServiceProvider;
 use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\CMS\Core\Type\Map;
 use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer;
 
 /**
@@ -105,6 +106,7 @@ class ServiceProvider extends AbstractServiceProvider
             'globalPageTsConfig' => [ static::class, 'getGlobalPageTsConfig' ],
             'icons' => [ static::class, 'getIcons' ],
             'middlewares' => [ static::class, 'getMiddlewares' ],
+            'content.security.policies' => [ static::class, 'getContentSecurityPolicies' ],
         ];
     }
 
@@ -568,6 +570,11 @@ class ServiceProvider extends AbstractServiceProvider
         return new ArrayObject();
     }
 
+    public static function getContentSecurityPolicies(ContainerInterface $container): Map
+    {
+        return new Map();
+    }
+
     public static function provideFallbackEventDispatcher(
         ContainerInterface $container,
         EventDispatcherInterface $eventDispatcher = null
diff --git a/typo3/sysext/core/Classes/Utility/DebugUtility.php b/typo3/sysext/core/Classes/Utility/DebugUtility.php
index 46e5dd6d6eaf27adc0404b12ac1935998e0b3e23..560482de678a6bc8a51739bcd87017752e4ad92b 100644
--- a/typo3/sysext/core/Classes/Utility/DebugUtility.php
+++ b/typo3/sysext/core/Classes/Utility/DebugUtility.php
@@ -18,6 +18,7 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Utility;
 
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Core\RequestId;
 use TYPO3\CMS\Extbase\Utility\DebuggerUtility;
 
 /**
@@ -95,7 +96,7 @@ class DebugUtility
 				}
 			})();
 		';
-        echo GeneralUtility::wrapJS($script);
+        echo GeneralUtility::wrapJS($script, ['nonce' => self::resolveNonceValue()]);
     }
 
     /**
@@ -220,4 +221,9 @@ class DebugUtility
     {
         static::$ansiColorUsage = $ansiColorUsage;
     }
+
+    protected static function resolveNonceValue(): string
+    {
+        return GeneralUtility::makeInstance(RequestId::class)->nonce->b64;
+    }
 }
diff --git a/typo3/sysext/core/Classes/Utility/GeneralUtility.php b/typo3/sysext/core/Classes/Utility/GeneralUtility.php
index c9a4380fd3898b45fc7f19e7612ac886247eab58..bab6bf923b6623decdda94ed3be3640744cfb809 100644
--- a/typo3/sysext/core/Classes/Utility/GeneralUtility.php
+++ b/typo3/sysext/core/Classes/Utility/GeneralUtility.php
@@ -1131,9 +1131,10 @@ class GeneralUtility
      * This is nice for indenting JS code with PHP code on the same level.
      *
      * @param string $string JavaScript code
+     * @param array<string, string> $attributes (optional) script tag HTML attributes
      * @return string The wrapped JS code, ready to put into a XHTML page
      */
-    public static function wrapJS($string)
+    public static function wrapJS(string $string, array $attributes = [])
     {
         if (trim($string)) {
             // remove nl from the beginning
@@ -1143,7 +1144,7 @@ class GeneralUtility
             if (preg_match('/^(\\t+)/', $string, $match)) {
                 $string = str_replace($match[1], "\t", $string);
             }
-            return '<script>
+            return '<script ' . GeneralUtility::implodeAttributes($attributes, true) . '>
 /*<![CDATA[*/
 ' . $string . '
 /*]]>*/
diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php
index 0e6a4e8620f31a7af6fef62e8bea9af5c03bea34..ed54d2a618c6c6480d2d6988b8dc0ede846c6868 100644
--- a/typo3/sysext/core/Configuration/DefaultConfiguration.php
+++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php
@@ -74,6 +74,8 @@ return [
             'redirects.hitCount' => false,
             'security.backend.htmlSanitizeRte' => false,
             'security.backend.enforceReferrer' => true,
+            'security.backend.enforceContentSecurityPolicy' => false,
+            'security.frontend.enforceContentSecurityPolicy' => false,
         ],
         'createGroup' => '',
         'sitename' => 'TYPO3',
@@ -1349,9 +1351,6 @@ return [
                     'strictTransportSecurity' => 'Strict-Transport-Security: max-age=31536000',
                     'avoidMimeTypeSniffing' => 'X-Content-Type-Options: nosniff',
                     'referrerPolicy' => 'Referrer-Policy: strict-origin-when-cross-origin',
-                    // 'csp-report' => "Content-Security-Policy-Report-Only: default-src 'self'; style-src-attr 'unsafe-inline'; img-src 'self' data:",
-                    // @todo laterâ„¢: muuri.js is creating workers from `blob:` (?!?), <style> tags declare inline styles (?!?)
-                    // 'csp-report' => "Content-Security-Policy-Report-Only: default-src 'self'; style-src-attr 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' data:; worker-src 'self' blob:;",
                 ],
             ],
         ],
diff --git a/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml
index 73913570e58d10959d6dde0ff50832f4d77a60fd..c9c6202f542b446aec5e3d495f892437556bb0fb 100644
--- a/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml
+++ b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml
@@ -204,6 +204,12 @@ SYS:
                 description: 'If on, HTTP referrer headers are enforced for backend and install tool requests to mitigate
                   potential same-site request forgery attacks. The behavior can be disabled in case HTTP proxies filter
                   required `Referer` header. As this is a potential security risk, it is recommended to enable this option.'
+              security.backend.enforceContentSecurityPolicy:
+                type: bool
+                description: 'If on, HTTP Content-Security-Policy header will be applied for each HTTP backend request.'
+              security.frontend.enforceContentSecurityPolicy:
+                type: bool
+                description: 'If on, HTTP Content-Security-Policy header will be applied for each HTTP frontend request.'
         availablePasswordHashAlgorithms:
             type: array
             description: 'A list of available password hash mechanisms. Extensions may register additional mechanisms here. This is usually not extended in system/settings.php.'
diff --git a/typo3/sysext/core/Configuration/FactoryConfiguration.php b/typo3/sysext/core/Configuration/FactoryConfiguration.php
index 740e548a468924f350dbb0f0f58fc012f41cc112..e38327553df20237f5f679bc28ae405f820f0772 100644
--- a/typo3/sysext/core/Configuration/FactoryConfiguration.php
+++ b/typo3/sysext/core/Configuration/FactoryConfiguration.php
@@ -23,5 +23,8 @@ return [
     'SYS' => [
         'sitename' => 'New TYPO3 site',
         'UTF8filesystem' => true,
+        'features' => [
+            'security.backend.enforceContentSecurityPolicy' => true,
+        ],
     ],
 ];
diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml
index 3340bdb6cdbaa24bbe5aca78f3cf13f76e452f73..4ad0ffbe08705691430d4ccabcd85a99545c4072 100644
--- a/typo3/sysext/core/Configuration/Services.yaml
+++ b/typo3/sysext/core/Configuration/Services.yaml
@@ -521,6 +521,13 @@ services:
     alias: TYPO3\CMS\Core\Mail\Mailer
     public: true
 
+  # Content-Security-Policy Handlers
+
+  TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyProvider:
+    public: true
+    arguments:
+      $mutations: '@content.security.policies'
+
   # External dependencies
 
   GuzzleHttp\Client:
diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Feature-99499-IntroduceContent-Security-PolicyHandling.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-99499-IntroduceContent-Security-PolicyHandling.rst
new file mode 100644
index 0000000000000000000000000000000000000000..9d6c7ef745615561bc123637dc3130373a34ba26
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-99499-IntroduceContent-Security-PolicyHandling.rst
@@ -0,0 +1,160 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-99499-1677703100:
+
+============================================================
+Feature: #99499 - Introduce Content-Security-Policy Handling
+============================================================
+
+See :issue:`99499`
+
+Description
+===========
+
+Corresponding representation of the W3C standard of
+`Content-Security-Policy (CSP) <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy>`
+has been introduced to TYPO3. Content-Security-Policy declarations can either be provided by using the
+the general builder pattern of :php:`\TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy`, extension
+specific mutations (changes to the general policy) via :file:`Configuration/ContentSecurityPolicies.php`
+located in corresponding extension directories, or YAML path :yaml:`contentSecurityPolicies.mutations` for
+site-specific declarations in the website frontend.
+
+The PSR-15 middlewares :php:`ContentSecurityPolicyHeaders` are applying `Content-Security-Policy` HTTP headers
+to each response in the frontend and backend scope. In case other components have already added either the
+header `Content-Security-Policy` or `Content-Security-Policy-Report-Only`, those existing headers will be
+kept without any modification - these events will be logged with an `info` severity.
+
+To delegate CSP handling to TYPO3, the scope-specific feature flags need to be enabled:
+
+* :php:`$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.backend.enforceContentSecurityPolicy']`
+* :php:`$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.enforceContentSecurityPolicy']`
+
+For new installations `security.backend.enforceContentSecurityPolicy` is enabled via factory default settings.
+
+Impact
+======
+
+Introducing CSP to TYPO3 aims to reduces the risk of being affected by Cross-Site-Scripting
+due to the lack of proper encoding of user-submitted content in corresponding outputs.
+
+Configuration
+=============
+
+`Policy` builder approach
+-------------------------
+
+..  code-block:: php
+
+    <?php
+    use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
+    use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy;
+    use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
+    use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
+    use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
+    use TYPO3\CMS\Core\Security\Nonce;
+
+    $nonce = Nonce::create();
+    $policy = (new Policy($nonce))
+        // results in `default-src 'self'`
+        ->default(SourceKeyword::self)
+        // extends the ancestor directive ('default-src'), thus reuses 'self' and adds additional sources
+        // results in `img-src 'self' data: https://*.typo3.org`
+        ->extend(Directive::ImgSrc, SourceScheme::data, new UriValue('https://*.typo3.org'))
+        // extends the ancestor directive ('default-src'), thus reuses 'self' and adds additional sources
+        // results in `script-src 'self' 'nonce-[random]'` ('nonce-proxy' is substituted when compiling the policy)
+        ->extend(Directive::ScriptSrc, SourceKeyword::nonceProxy)
+        // sets (overrides) the directive, thus ignores 'self' of the 'default-src' directive
+        // results in `worker-src blob:`
+        ->set(Directive::WorkerSrc, SourceScheme::blob);
+    header('Content-Security-Policy: ' . (string)$policy);
+
+The result of the compiled and serialized result as HTTP header would look similar to this
+(the following sections are using the same example, but utilize different techniques for the declarations).
+
+..  code-block::
+
+    Content-Security-Policy: default-src 'self';
+        img-src 'self' data: https://*.typo3.org; script-src 'self' 'nonce-[random]';
+        worker-src blob:
+
+Extension specific
+------------------
+
+Having a file :file:`Configuration/ContentSecurityPolicies.php` in the base directory
+of any extension declared, will automatically provide and apply corresponding settings.
+
+..  code-block:: php
+
+    <?php
+    use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
+    use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
+    use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
+    use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
+    use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
+    use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
+    use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
+    use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
+    use TYPO3\CMS\Core\Type\Map;
+
+    return Map::fromEntries([
+        // provide declarations for the backend
+        Scope::backend(),
+        // NOTICE: When using `MutationMode::Set` existing declarations will be overridden
+        new MutationCollection(
+            // results in `default-src 'self'`
+            new Mutation(MutationMode::Set, Directive::DefaultSrc, SourceKeyword::self),
+            // extends the ancestor directive ('default-src'), thus reuses 'self' and adds additional sources
+            // results in `img-src 'self' data: https://*.typo3.org`
+            new Mutation(MutationMode::Extend, Directive::ImgSrc, SourceScheme::data, new UriValue('https://*.typo3.org')),
+            // extends the ancestor directive ('default-src'), thus reuses 'self' and adds additional sources
+            // results in `script-src 'self' 'nonce-[random]'` ('nonce-proxy' is substituted when compiling the policy)
+            new Mutation(MutationMode::Extend, Directive::ScriptSrc, SourceKeyword::nonceProxy),
+            // sets (overrides) the directive, thus ignores 'self' of the 'default-src' directive
+            // results in `worker-src blob:`
+            new Mutation(MutationMode::Set, Directive::WorkerSrc, SourceScheme::blob),
+        ),
+    ]);
+
+Site-specific (frontend)
+------------------------
+
+In the frontend, the dedicated :file:`sites/my-site/csp.yaml` can be used to declare CSP for a specific site as well.
+
+..  code-block:: yaml
+
+    # inherits default site-unspecific frontend policy mutations (enabled per default)
+    inheritDefault: true
+    mutations:
+      # results in `default-src 'self'`
+      - mode: set
+        directive: 'default-src'
+        sources:
+          - "'self'"
+      # extends the ancestor directive ('default-src'), thus reuses 'self' and adds additional sources
+      # results in `img-src 'self' data: https://*.typo3.org`
+      - mode: extend
+        directive: 'img-src'
+        sources:
+          - 'data:'
+          - 'https://*.typo3.org'
+      # extends the ancestor directive ('default-src'), thus reuses 'self' and adds additional sources
+      # results in `script-src 'self' 'nonce-[random]'` ('nonce-proxy' is substituted when compiling the policy)
+      - mode: extend
+        directive: 'script-src'
+        sources:
+          - "'nonce-proxy'"
+      # results in `worker-src blob:`
+      - mode: set
+        directive: 'worker-src'
+        sources:
+          - 'blob:'
+
+PSR-14 Events
+=============
+
+The :php:`\TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyMutatedEvent` will
+be dispatched once all mutations have been applied to the current policy object, just
+before the corresponding HTTP header is added to the HTTP response object.
+This allows individual adjustments for custom implementations.
+
+.. index:: Backend, Fluid, Frontend, LocalConfiguration, PHP-API, ext:core
diff --git a/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php b/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php
index 4f1cc1013b214c64123060f83c0f6339bd2a84bb..9051d1172960c3adfa18229f372dab82e189609b 100644
--- a/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php
+++ b/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php
@@ -79,19 +79,9 @@ final class ApplicationEnvironment extends BackendEnvironment
             'MAIL' => [
                 'transport' => NullTransport::class,
             ],
-            'BE' => [
-                'HTTP' => [
-                    'Response' => [
-                        'Headers' => [
-                            // Notes:
-                            //  * `script-src 'nonce-rAnd0m'` required for importmap
-                            //                                todo: this needs to be a proper random value, requires API.
-                            //  * `frame-src blob:` required for es-module-shims blob: URLs
-                            //  * `style-src 'unsafe-inline'` required for lit in safari and firefox to allow inline <style> tags
-                            //                (for browsers that do not support https://caniuse.com/mdn-api_shadowroot_adoptedstylesheets)
-                            'csp-report' => "Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-rAnd0m'; style-src 'self' 'unsafe-inline'; style-src-attr 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' data:; worker-src 'self' blob:; frame-src 'self' blob:;",
-                        ],
-                    ],
+            'SYS' => [
+                'features' => [
+                    'security.backend.enforceContentSecurityPolicy' => true,
                 ],
             ],
         ],
diff --git a/typo3/sysext/core/Tests/Unit/Http/Security/ReferrerEnforcerTest.php b/typo3/sysext/core/Tests/Unit/Http/Security/ReferrerEnforcerTest.php
index 5c2fcc55bc8247c97260300e7f934129246c677b..0202da58f6b978ebae291959153a5c1cc75d4468 100644
--- a/typo3/sysext/core/Tests/Unit/Http/Security/ReferrerEnforcerTest.php
+++ b/typo3/sysext/core/Tests/Unit/Http/Security/ReferrerEnforcerTest.php
@@ -23,6 +23,7 @@ use TYPO3\CMS\Core\Http\Security\InvalidReferrerException;
 use TYPO3\CMS\Core\Http\Security\MissingReferrerException;
 use TYPO3\CMS\Core\Http\Security\ReferrerEnforcer;
 use TYPO3\CMS\Core\Http\Uri;
+use TYPO3\CMS\Core\Security\Nonce;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 class ReferrerEnforcerTest extends UnitTestCase
@@ -177,7 +178,25 @@ class ReferrerEnforcerTest extends UnitTestCase
         $subject->handle();
     }
 
-    private function buildSubject(string $requestUri, string $referrer): ReferrerEnforcer
+    /**
+     * @test
+     */
+    public function nonceIsAppliedToResponse(): void
+    {
+        $nonce = Nonce::create();
+        $subject = $this->buildSubject(
+            'https://example.org/typo3/login',
+            '',
+            $nonce
+        );
+        $response = $subject->handle(['flags' => ['refresh-always']]);
+        self::assertStringContainsString(
+            'nonce="' . htmlspecialchars($nonce->b64) . '">',
+            (string)$response->getBody()
+        );
+    }
+
+    private function buildSubject(string $requestUri, string $referrer, Nonce $nonce = null): ReferrerEnforcer
     {
         $requestUriInstance = new Uri($requestUri);
         $host = sprintf(
@@ -192,11 +211,20 @@ class ReferrerEnforcerTest extends UnitTestCase
         $normalizedParams->method('getRequestHost')->willReturn($host);
         $normalizedParams->method('getRequestDir')->willReturn($dir);
         $request = $this->createMock(ServerRequestInterface::class);
-        $request->method('getAttribute')->with('normalizedParams')->willReturn($normalizedParams);
+        $request->method('getAttribute')->willReturnCallback(static fn (string $name) => match ($name) {
+            'normalizedParams' => $normalizedParams,
+            'nonce' => $nonce,
+            default => null,
+        });
         $request->method('getServerParams')->willReturn(['HTTP_REFERER' => $referrer]);
         $request->method('getUri')->willReturn($requestUriInstance);
         $request->method('getQueryParams')->willReturn($queryParams);
 
-        return new ReferrerEnforcer($request);
+        $mock = $this->getMockBuilder(ReferrerEnforcer::class)
+            ->onlyMethods(['resolveAbsoluteWebPath'])
+            ->setConstructorArgs([$request])
+            ->getMock();
+        $mock->method('resolveAbsoluteWebPath')->willReturnCallback(static fn (string $path) => '/' . $path);
+        return $mock;
     }
 }
diff --git a/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/PolicyTest.php b/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/PolicyTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..aa843b10cdd46cc4e3940944737e03d3c7944444
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/PolicyTest.php
@@ -0,0 +1,145 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Tests\Unit\Security\ContentSecurityPolicy;
+
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
+use TYPO3\CMS\Core\Security\Nonce;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class PolicyTest extends UnitTestCase
+{
+    private Nonce $nonce;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->nonce = Nonce::create();
+    }
+
+    /**
+     * @test
+     */
+    public function constructorSetsdefaultDirective(): void
+    {
+        $policy = (new Policy($this->nonce, SourceKeyword::self));
+        self::assertSame("default-src 'self'", (string)$policy);
+    }
+
+    /**
+     * @test
+     */
+    public function defaultDirectiveIsModified(): void
+    {
+        $policy = (new Policy($this->nonce, SourceKeyword::self))
+            ->default(SourceKeyword::none);
+        self::assertSame("default-src 'none'", (string)$policy);
+    }
+
+    /**
+     * @test
+     */
+    public function defaultDirectiveConsidersVeto(): void
+    {
+        $policy = (new Policy($this->nonce, SourceKeyword::self))
+            ->default(SourceKeyword::unsafeEval, SourceKeyword::none);
+        self::assertSame("default-src 'none'", (string)$policy);
+    }
+
+    /**
+     * @test
+     */
+    public function newDirectiveExtendsDefault(): void
+    {
+        $policy = (new Policy($this->nonce, SourceKeyword::self))
+            ->extend(Directive::ScriptSrc, SourceKeyword::unsafeInline);
+        self::assertSame("default-src 'self'; script-src 'self' 'unsafe-inline'", (string)$policy);
+    }
+
+    /**
+     * @test
+     */
+    public function newDirectiveDoesNotExtendDefault(): void
+    {
+        $policy = (new Policy($this->nonce, SourceKeyword::self))
+            ->set(Directive::ScriptSrc, SourceKeyword::unsafeInline);
+        self::assertSame("default-src 'self'; script-src 'unsafe-inline'", (string)$policy);
+    }
+
+    /**
+     * @test
+     */
+    public function sourceSchemeIsCompiled(): void
+    {
+        $policy = (new Policy($this->nonce, SourceKeyword::self, SourceScheme::blob));
+        self::assertSame("default-src 'self' blob:", (string)$policy);
+    }
+
+    /**
+     * @test
+     */
+    public function nonceProxyIsCompiled(): void
+    {
+        $policy = (new Policy($this->nonce, SourceKeyword::self, SourceKeyword::nonceProxy));
+        self::assertSame("default-src 'self' 'nonce-{$this->nonce->b64}'", (string)$policy);
+    }
+
+    /**
+     * @test
+     */
+    public function directiveIsRemoved(): void
+    {
+        $policy = (new Policy($this->nonce, SourceKeyword::self))
+            ->remove(Directive::DefaultSrc);
+        self::assertSame('', (string)$policy);
+    }
+
+    /**
+     * @test
+     */
+    public function superfluousDirectivesArePurged(): void
+    {
+        $policy = (new Policy($this->nonce, SourceKeyword::self, SourceScheme::data))
+            ->set(Directive::ScriptSrc, SourceKeyword::self, SourceScheme::data);
+        self::assertSame("default-src 'self' data:", (string)$policy);
+    }
+
+    /**
+     * @test
+     */
+    public function backendPolicyIsCompiled(): void
+    {
+        $nonce = Nonce::create();
+        $policy = (new Policy($this->nonce))
+            ->default(SourceKeyword::self)
+            ->extend(Directive::ScriptSrc, $nonce)
+            ->extend(Directive::StyleSrc, SourceKeyword::unsafeInline)
+            ->set(Directive::StyleSrcAttr, SourceKeyword::unsafeInline)
+            ->extend(Directive::ImgSrc, SourceScheme::data)
+            ->set(Directive::WorkerSrc, SourceKeyword::self, SourceScheme::blob)
+            ->extend(Directive::FrameSrc, SourceScheme::blob);
+        self::assertSame(
+            "default-src 'self'; script-src 'self' 'nonce-{$nonce->b64}'; "
+            . "style-src 'self' 'unsafe-inline'; style-src-attr 'unsafe-inline'; "
+            . "img-src 'self' data:; worker-src 'self' blob:; frame-src 'self' blob:",
+            (string)$policy
+        );
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/ScopeTest.php b/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/ScopeTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..7a108851f64f30f911827917e4b6d6278f5c582a
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/ScopeTest.php
@@ -0,0 +1,105 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Tests\Unit\Security\ContentSecurityPolicy;
+
+use TYPO3\CMS\Core\Http\ApplicationType;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
+use TYPO3\CMS\Core\Site\Entity\NullSite;
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\Entity\SiteInterface;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class ScopeTest extends UnitTestCase
+{
+    protected function tearDown(): void
+    {
+        parent::tearDown();
+        Scope::reset();
+    }
+
+    /**
+     * @test
+     */
+    public function backendSingletonIsCreated(): void
+    {
+        $scope = Scope::backend();
+        self::assertSame($scope, Scope::backend());
+    }
+
+    /**
+     * @test
+     */
+    public function frontendSingletonIsCreated(): void
+    {
+        $scope = Scope::frontend();
+        self::assertSame($scope, Scope::frontend());
+    }
+
+    /**
+     * @test
+     */
+    public function frontendSiteIsCreated(): void
+    {
+        $site = new Site('my-site', 1, []);
+        $scope = Scope::frontendSite($site);
+        self::assertSame($scope, Scope::frontendSite($site));
+    }
+
+    public static function frontendSingletonIsUsedForInvalidSiteDataProvider(): \Generator
+    {
+        yield [null];
+        yield [new NullSite()];
+    }
+
+    /**
+     * @test
+     * @dataProvider frontendSingletonIsUsedForInvalidSiteDataProvider
+     */
+    public function frontendSingletonIsUsedForInvalidSite(?SiteInterface $site): void
+    {
+        self::assertSame(Scope::frontend(), Scope::frontendSite($site));
+    }
+
+    /**
+     * @test
+     */
+    public function frontendSiteIdentifierSingletonIsCreated(): void
+    {
+        $scope = Scope::frontendSiteIdentifier('my-site');
+        self::assertSame($scope, Scope::frontendSiteIdentifier('my-site'));
+    }
+
+    /**
+     * @test
+     */
+    public function scopeIsCreatedFromString(): void
+    {
+        self::assertSame(
+            Scope::frontend(),
+            Scope::from(ApplicationType::FRONTEND->value)
+        );
+        self::assertSame(
+            Scope::backend(),
+            Scope::from(ApplicationType::BACKEND->value)
+        );
+        self::assertSame(
+            Scope::frontendSiteIdentifier('my-site'),
+            Scope::from(ApplicationType::FRONTEND->value . '.my-site')
+        );
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/UriValueTest.php b/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/UriValueTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..384cd9ef68778cee9f8caebe591d66ace9d63658
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/UriValueTest.php
@@ -0,0 +1,59 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Tests\Unit\Security\ContentSecurityPolicy;
+
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class UriValueTest extends UnitTestCase
+{
+    public static function uriIsParsedAndSerializedDataProvider(): \Generator
+    {
+        yield ['https://www.typo3.org/uri/path.html?key=value#fragment', 'https://www.typo3.org/uri/path.html?key=value'];
+        yield ['//www.typo3.org/uri/path.html?key=value#fragment', '//www.typo3.org/uri/path.html?key=value'];
+        yield ['https://www.typo3.org#fragment', 'https://www.typo3.org'];
+        yield ['//www.typo3.org#fragment', '//www.typo3.org'];
+        yield ['https://*.typo3.org#fragment', 'https://*.typo3.org'];
+        yield ['//*.typo3.org#fragment', '//*.typo3.org'];
+        yield ['www.typo3.org#fragment', 'www.typo3.org'];
+        yield ['*.typo3.org#fragment', '*.typo3.org'];
+
+        yield ['https://www.typo3.org/uri/path.html?key=value'];
+        yield ['https://www.typo3.org'];
+        yield ['https://*.typo3.org'];
+        yield ['//www.typo3.org/uri/path.html?key=value'];
+        yield ['//www.typo3.org'];
+        yield ['www.typo3.org'];
+        yield ['*.typo3.org'];
+
+        // expected behavior, falls back to upstream parser´
+        // (since e.g. query-param is given, which is not expected here in the scope of CSP with `UriValue`)
+        yield ['www.typo3.org?key=value', '/www.typo3.org?key=value'];
+        yield ['*.typo3.org?key=value', '/%2A.typo3.org?key=value'];
+    }
+
+    /**
+     * @test
+     * @dataProvider uriIsParsedAndSerializedDataProvider
+     */
+    public function uriIsParsedAndSerialized(string $value, string $expectation = null): void
+    {
+        $uri = new UriValue($value);
+        self::assertSame($expectation ?? $value, (string)$uri);
+    }
+}
diff --git a/typo3/sysext/extbase/Classes/Utility/DebuggerUtility.php b/typo3/sysext/extbase/Classes/Utility/DebuggerUtility.php
index ab6e3d5c9b94318ff172e4f15f8322c0fad4898a..2346f035a057f8deb6fee5b83ea778e06ed3ff40 100644
--- a/typo3/sysext/extbase/Classes/Utility/DebuggerUtility.php
+++ b/typo3/sysext/extbase/Classes/Utility/DebuggerUtility.php
@@ -17,7 +17,9 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Extbase\Utility;
 
+use TYPO3\CMS\Core\Core\RequestId;
 use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
 use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
 use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
@@ -285,7 +287,7 @@ class DebuggerUtility
                 $dump .= '<span class="extbase-debug-filtered">filtered</span>';
             }
         } elseif (self::$renderedObjects->contains($object) && !$plainText) {
-            $dump = '<a href="javascript:;" onclick="document.location.hash=\'#' . spl_object_hash($object) . '\';" class="extbase-debug-seeabove">' . $dump . '<span class="extbase-debug-filtered">see above</span></a>';
+            $dump = '<a href="#' . spl_object_hash($object) . '" class="extbase-debug-seeabove">' . $dump . '<span class="extbase-debug-filtered">see above</span></a>';
         } elseif ($level >= self::$maxDepth && !$object instanceof \DateTimeInterface) {
             if ($plainText) {
                 $dump .= ' ' . self::ansiEscapeWrap('max depth', '47;30', $ansiColors);
@@ -521,8 +523,11 @@ class DebuggerUtility
         self::clearState();
         $css = '';
         if (!$plainText && self::$stylesheetEchoed === false) {
+            $attributes = GeneralUtility::implodeAttributes([
+                'nonce' => self::resolveNonceValue(),
+            ], true);
             $css = '
-				<style>
+				<style ' . $attributes . '>
 					.extbase-debugger-tree{position:relative}
 					.extbase-debugger-tree input{position:absolute !important;float: none !important;top:0;left:0;height:14px;width:14px;margin:0 !important;cursor:pointer;opacity:0;z-index:2}
 					.extbase-debugger-tree input~.extbase-debug-content{display:none}
@@ -572,4 +577,9 @@ class DebuggerUtility
 
         return '';
     }
+
+    protected static function resolveNonceValue(): string
+    {
+        return GeneralUtility::makeInstance(RequestId::class)->nonce->b64;
+    }
 }
diff --git a/typo3/sysext/extbase/Tests/Unit/Utility/DebuggerUtilityTest.php b/typo3/sysext/extbase/Tests/Unit/Utility/DebuggerUtilityTest.php
index f6fdaeff6be1b1120ef08280cb58eefc7a00a896..98c22326176186ddde12fccb3afd2e2325fe3b68 100644
--- a/typo3/sysext/extbase/Tests/Unit/Utility/DebuggerUtilityTest.php
+++ b/typo3/sysext/extbase/Tests/Unit/Utility/DebuggerUtilityTest.php
@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Extbase\Tests\Unit\Utility;
 
 use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
 use TYPO3\CMS\Extbase\Tests\Fixture\DummyClass;
+use TYPO3\CMS\Extbase\Tests\Unit\Utility\Fixtures\DebuggerUtilityAccessibleProxy;
 use TYPO3\CMS\Extbase\Utility\DebuggerUtility;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
@@ -201,4 +202,18 @@ class DebuggerUtilityTest extends UnitTestCase
         $result = DebuggerUtility::var_dump($class, null, 8, true, false, true);
         self::assertStringContainsString('test => protected uninitialized', $result);
     }
+
+    /**
+     * @test
+     */
+    public function varDumpUsesNonceValue(): void
+    {
+        DebuggerUtilityAccessibleProxy::setStylesheetEchoed(false);
+        $class = new class () {
+            protected \stdClass $test;
+        };
+        $result = DebuggerUtilityAccessibleProxy::var_dump($class, null, 8, false, false, true);
+        self::assertTrue(DebuggerUtilityAccessibleProxy::getStylesheetEchoed());
+        self::assertMatchesRegularExpression('#<style nonce="[^"]+">[^>]+</style>#m', $result);
+    }
 }
diff --git a/typo3/sysext/extbase/Tests/Unit/Utility/Fixtures/DebuggerUtilityAccessibleProxy.php b/typo3/sysext/extbase/Tests/Unit/Utility/Fixtures/DebuggerUtilityAccessibleProxy.php
new file mode 100644
index 0000000000000000000000000000000000000000..6eec2ab302b5e96d7c0ebe858d5cac4ab422d0f5
--- /dev/null
+++ b/typo3/sysext/extbase/Tests/Unit/Utility/Fixtures/DebuggerUtilityAccessibleProxy.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Extbase\Tests\Unit\Utility\Fixtures;
+
+use TYPO3\CMS\Extbase\Utility\DebuggerUtility;
+
+/**
+ * Accessible proxy with getters/setters for protected static properties.
+ */
+class DebuggerUtilityAccessibleProxy extends DebuggerUtility
+{
+    public static function getStylesheetEchoed(): bool
+    {
+        return self::$stylesheetEchoed;
+    }
+
+    public static function setStylesheetEchoed(bool $stylesheetEchoed): void
+    {
+        self::$stylesheetEchoed = $stylesheetEchoed;
+    }
+}
diff --git a/typo3/sysext/filelist/Configuration/ContentSecurityPolicies.php b/typo3/sysext/filelist/Configuration/ContentSecurityPolicies.php
new file mode 100644
index 0000000000000000000000000000000000000000..69d84790f0df525a5a3e1272aa9efdb43ddd0863
--- /dev/null
+++ b/typo3/sysext/filelist/Configuration/ContentSecurityPolicies.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Backend;
+
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
+use TYPO3\CMS\Core\Type\Map;
+
+/**
+ * Allows to fetch media assets from YouTube and Vimeo and their associated CDNs,
+ * to be embedded in an `<iframe>` of the corresponding info modal in the file list
+ * backend module.
+ */
+return Map::fromEntries([
+    Scope::backend(),
+    new MutationCollection(
+        new Mutation(
+            MutationMode::Extend,
+            Directive::FrameSrc,
+            new UriValue('*.youtube-nocookie.com'),
+            new UriValue('*.youtube.com'),
+            new UriValue('*.vimeo.com')
+        ),
+        // @todo this still shows violations like the following when opened in the info modal
+        // > Refused to load the image 'https://i.ytimg.com/mqdefault.jpg' because it violates the
+        // > following Content Security Policy directive: "img-src 'self' 'self' 'self' data: *.i.ytimg.com
+        // + no problem when directly playing video via `/typo3/record/info` in new tab (not in modal)
+        // + fine in Safari/macOS, fails in Chrome/macOS, ...
+        new Mutation(
+            MutationMode::Extend,
+            Directive::ImgSrc,
+            new UriValue('*.ytimg.com'),
+            new UriValue('*.vimeocdn.com')
+        ),
+    ),
+]);
diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Security/NonceViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Security/NonceViewHelper.php
new file mode 100644
index 0000000000000000000000000000000000000000..6488e8e1c930cdfd054dd96ed3cab8c2d5130b04
--- /dev/null
+++ b/typo3/sysext/fluid/Classes/ViewHelpers/Security/NonceViewHelper.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Fluid\ViewHelpers\Security;
+
+use TYPO3\CMS\Core\Core\RequestId;
+use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
+
+/**
+ * This ViewHelper resolves the `nonce` attribute from the global server request object,
+ * or from the `PolicyProvider` service as a fall-back value.
+ *
+ * Examples
+ * ========
+ *
+ * Basic usage
+ * -----------
+ *
+ * ::
+ *
+ *    <script nonce="{f:security.nonce()}">const inline = 'script';</script>
+ */
+final class NonceViewHelper extends AbstractViewHelper
+{
+    public function __construct(private readonly RequestId $requestId)
+    {
+    }
+
+    public function initializeArguments(): void
+    {
+        parent::initializeArguments();
+    }
+
+    public function render(): string
+    {
+        return $this->requestId->nonce->b64;
+    }
+}
diff --git a/typo3/sysext/frontend/Classes/Middleware/ContentSecurityPolicyHeaders.php b/typo3/sysext/frontend/Classes/Middleware/ContentSecurityPolicyHeaders.php
new file mode 100644
index 0000000000000000000000000000000000000000..e4ba86b19a568f0fd753db337a01f2c711025e86
--- /dev/null
+++ b/typo3/sysext/frontend/Classes/Middleware/ContentSecurityPolicyHeaders.php
@@ -0,0 +1,70 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Frontend\Middleware;
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use Psr\Log\LoggerInterface;
+use TYPO3\CMS\Core\Configuration\Features;
+use TYPO3\CMS\Core\Core\RequestId;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyProvider;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
+
+/**
+ * Adds Content-Security-Policy headers to response.
+ *
+ * @internal
+ */
+final class ContentSecurityPolicyHeaders implements MiddlewareInterface
+{
+    public function __construct(
+        private readonly Features $features,
+        private readonly RequestId $requestId,
+        private readonly LoggerInterface $logger,
+        private readonly PolicyProvider $policyProvider,
+    ) {
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        $request = $request->withAttribute('nonce', $this->requestId->nonce);
+        $response = $handler->handle($request);
+
+        if (!$this->features->isFeatureEnabled('security.frontend.enforceContentSecurityPolicy')) {
+            return $response;
+        }
+
+        $site = $request->getAttribute('site');
+        $scope = Scope::frontendSite($site);
+        if ($response->hasHeader('Content-Security-Policy') || $response->hasHeader('Content-Security-Policy-Report-Only')) {
+            $this->logger->info('Content-Security-Policy not enforced due to existence of custom header', [
+                'scope' => (string)$scope,
+                'uri' => (string)$request->getUri(),
+            ]);
+            return $response;
+        }
+
+        $policy = $this->policyProvider->provideFor($scope);
+        if ($policy->isEmpty()) {
+            return $response;
+        }
+        return $response->withHeader('Content-Security-Policy', (string)$policy);
+    }
+}
diff --git a/typo3/sysext/frontend/Configuration/ContentSecurityPolicies.php b/typo3/sysext/frontend/Configuration/ContentSecurityPolicies.php
new file mode 100644
index 0000000000000000000000000000000000000000..059e3d3a7b6d57c75a0782ca2379f26d4a9b3325
--- /dev/null
+++ b/typo3/sysext/frontend/Configuration/ContentSecurityPolicies.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Backend;
+
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
+use TYPO3\CMS\Core\Type\Map;
+
+/**
+ * Provides a simple basic Content-Security-Policy for the generic frontend scope.
+ */
+return Map::fromEntries([
+    Scope::frontend(),
+    new MutationCollection(
+        new Mutation(MutationMode::Extend, Directive::DefaultSrc, SourceKeyword::self),
+        new Mutation(MutationMode::Extend, Directive::ScriptSrc, SourceKeyword::nonceProxy),
+        // `style-src-attr 'unsafe-inline'` required for remaining inline styles, which is okay for color & dimension
+        // (e.g. `<div style="color: #000">` - but NOT having the possibility to use any other assets/files/URIs)
+        new Mutation(MutationMode::Set, Directive::StyleSrcAttr, SourceKeyword::unsafeInline),
+        // allow `data:` images
+        new Mutation(MutationMode::Extend, Directive::ImgSrc, SourceScheme::data),
+    ),
+]);
diff --git a/typo3/sysext/frontend/Configuration/RequestMiddlewares.php b/typo3/sysext/frontend/Configuration/RequestMiddlewares.php
index ee1a0a972e12ab6e53885ee2280c08e20c1b0cea..47f4d4f58214ed1ac60d0d27a1d010f51b3173e5 100644
--- a/typo3/sysext/frontend/Configuration/RequestMiddlewares.php
+++ b/typo3/sysext/frontend/Configuration/RequestMiddlewares.php
@@ -171,10 +171,18 @@ return [
             ],
         ],
         /** internal: do not use or reference this middleware in your own code */
+        'typo3/cms-frontend/csp-headers' => [
+            'target' => \TYPO3\CMS\Frontend\Middleware\ContentSecurityPolicyHeaders::class,
+            'after' => [
+                'typo3/cms-frontend/prepare-tsfe-rendering',
+            ],
+        ],
+        /** internal: do not use or reference this middleware in your own code */
         'typo3/cms-core/response-propagation' => [
             'target' => \TYPO3\CMS\Core\Middleware\ResponsePropagation::class,
             'after' => [
                 'typo3/cms-frontend/output-compression',
+                'typo3/cms-frontend/csp-headers',
             ],
         ],
     ],
diff --git a/typo3/sysext/install/Classes/Controller/ControllerTrait.php b/typo3/sysext/install/Classes/Controller/ControllerTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..5e73b469e063a7ee1d255d7eb54210f4e2fb3eeb
--- /dev/null
+++ b/typo3/sysext/install/Classes/Controller/ControllerTrait.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Install\Controller;
+
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
+use TYPO3\CMS\Core\Security\Nonce;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * @internal This class is a specific implementation and is not considered part of the Public TYPO3 API.
+ */
+trait ControllerTrait
+{
+    /**
+     * Using fixed Content-Security-Policy for Admin Tool (extensions and database might not be available)
+     */
+    protected function createContentSecurityPolicy(Nonce $nonce): Policy
+    {
+        return GeneralUtility::makeInstance(Policy::class, $nonce)
+            ->default(SourceKeyword::self)
+            // script-src 'nonce-...' required for importmaps
+            ->extend(Directive::ScriptSrc, $nonce)
+            // `style-src 'unsafe-inline'` required for lit in safari and firefox to allow inline <style> tags
+            // (for browsers that do not support https://caniuse.com/mdn-api_shadowroot_adoptedstylesheets)
+            ->extend(Directive::StyleSrc, SourceKeyword::unsafeInline)
+            ->set(Directive::StyleSrcAttr, SourceKeyword::unsafeInline)
+            ->extend(Directive::ImgSrc, SourceScheme::data)
+            // `frame-src blob:` required for es-module-shims blob: URLs
+            ->extend(Directive::FrameSrc, SourceScheme::blob);
+    }
+}
diff --git a/typo3/sysext/install/Classes/Controller/InstallerController.php b/typo3/sysext/install/Classes/Controller/InstallerController.php
index cf0cfae9aa82a45362ca9f4c09538377c465d947..cfcee098a7bad5faf355756d5bd751a14207db93 100644
--- a/typo3/sysext/install/Classes/Controller/InstallerController.php
+++ b/typo3/sysext/install/Classes/Controller/InstallerController.php
@@ -37,6 +37,7 @@ use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
 use TYPO3\CMS\Core\Middleware\VerifyHostHeader;
 use TYPO3\CMS\Core\Package\FailsafePackageManager;
 use TYPO3\CMS\Core\Page\ImportMap;
+use TYPO3\CMS\Core\Security\Nonce;
 use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\View\FluidViewAdapter;
@@ -64,6 +65,8 @@ use TYPO3Fluid\Fluid\View\TemplateView as FluidTemplateView;
  */
 final class InstallerController
 {
+    use ControllerTrait;
+
     public function __construct(
         private readonly LateBootService $lateBootService,
         private readonly SilentConfigurationUpgradeService $silentConfigurationUpgradeService,
@@ -97,12 +100,14 @@ final class InstallerController
         $view = $this->initializeView();
         $view->assign('bust', $bust);
         $view->assign('initModule', $initModule);
-        $view->assign('importmap', $importMap->render($sitePath, 'rAnd0m'));
+        $nonce = Nonce::create();
+        $view->assign('importmap', $importMap->render($sitePath, $nonce->b64));
 
         return new HtmlResponse(
             $view->render('Installer/Init'),
             200,
             [
+                'Content-Security-Policy' => (string)$this->createContentSecurityPolicy($nonce),
                 'Cache-Control' => 'no-cache, must-revalidate',
                 'Pragma' => 'no-cache',
             ]
diff --git a/typo3/sysext/install/Classes/Controller/LayoutController.php b/typo3/sysext/install/Classes/Controller/LayoutController.php
index 2fe416bad9f5385292fa876e6be5d81ecc28a39e..a2aecf8ecd63c35385373e617aaeff17e52c8b56 100644
--- a/typo3/sysext/install/Classes/Controller/LayoutController.php
+++ b/typo3/sysext/install/Classes/Controller/LayoutController.php
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Http\JsonResponse;
 use TYPO3\CMS\Core\Information\Typo3Version;
 use TYPO3\CMS\Core\Package\FailsafePackageManager;
 use TYPO3\CMS\Core\Page\ImportMap;
+use TYPO3\CMS\Core\Security\Nonce;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\Service\Exception\ConfigurationChangedException;
 use TYPO3\CMS\Install\Service\Exception\TemplateFileChangedException;
@@ -41,6 +42,8 @@ use TYPO3\CMS\Install\Service\SilentTemplateFileUpgradeService;
  */
 class LayoutController extends AbstractController
 {
+    use ControllerTrait;
+
     private FailsafePackageManager $packageManager;
     private SilentConfigurationUpgradeService $silentConfigurationUpgradeService;
     private SilentTemplateFileUpgradeService $silentTemplateFileUpgradeService;
@@ -76,18 +79,20 @@ class LayoutController extends AbstractController
         $initModule = $sitePath . $importMap->resolveImport('@typo3/install/init-install.js');
 
         $view = $this->initializeView($request);
+        $nonce = Nonce::create();
         $view->assignMultiple([
             // time is used as cache bust for js and css resources
             'bust' => $bust,
             'siteName' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
             'initModule' => $initModule,
-            'importmap' => $importMap->render($sitePath, 'rAnd0m'),
+            'importmap' => $importMap->render($sitePath, $nonce->b64),
         ]);
         return new HtmlResponse(
             $view->render('Layout/Init'),
             200,
             [
                 'Cache-Control' => 'no-cache, must-revalidate',
+                'Content-Security-Policy' => (string)$this->createContentSecurityPolicy($nonce),
                 'Pragma' => 'no-cache',
             ]
         );