diff --git a/typo3/sysext/backend/Classes/Middleware/ContentSecurityPolicyHeaders.php b/typo3/sysext/backend/Classes/Middleware/ContentSecurityPolicyHeaders.php
index 64e1438f3aa4fa21938c62afe4979e75676957fc..c88d778f88ff3b7289ed2caf52deb2ac6db877d3 100644
--- a/typo3/sysext/backend/Classes/Middleware/ContentSecurityPolicyHeaders.php
+++ b/typo3/sysext/backend/Classes/Middleware/ContentSecurityPolicyHeaders.php
@@ -25,6 +25,7 @@ use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\Attribute\Autowire;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\CMS\Core\Core\RequestId;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Disposition;
 use TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyProvider;
 use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
 use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
@@ -58,7 +59,7 @@ final readonly class ContentSecurityPolicyHeaders implements MiddlewareInterface
             return $response;
         }
 
-        $policy = $this->policyProvider->provideFor($scope, $request);
+        $policy = $this->policyProvider->provideFor($scope, Disposition::enforce, $request);
         if ($policy->isEmpty()) {
             return $response;
         }
@@ -66,6 +67,9 @@ final readonly class ContentSecurityPolicyHeaders implements MiddlewareInterface
         if ($reportingUri !== null) {
             $policy = $policy->report(UriValue::fromUri($reportingUri));
         }
-        return $response->withHeader('Content-Security-Policy', $policy->compile($this->requestId->nonce, $this->cache));
+        return $response->withHeader(
+            Disposition::enforce->getHttpHeaderName(),
+            $policy->compile($this->requestId->nonce, $this->cache)
+        );
     }
 }
diff --git a/typo3/sysext/backend/Classes/Security/ContentSecurityPolicy/CspAjaxController.php b/typo3/sysext/backend/Classes/Security/ContentSecurityPolicy/CspAjaxController.php
index b2f61c64e1be4a6625b0e09a598d0e1ae4a7466b..2f77a0a2068b38d59ebe1ec839ae944a2abda5e2 100644
--- a/typo3/sysext/backend/Classes/Security/ContentSecurityPolicy/CspAjaxController.php
+++ b/typo3/sysext/backend/Classes/Security/ContentSecurityPolicy/CspAjaxController.php
@@ -58,6 +58,9 @@ class CspAjaxController
 
     public function handleRequest(ServerRequestInterface $request): ResponseInterface
     {
+        if ($request->getMethod() === 'GET') {
+            return (new NullResponse())->withStatus(400);
+        }
         if (!$this->isSystemMaintainer()) {
             return (new NullResponse())->withStatus(403);
         }
@@ -182,7 +185,8 @@ class CspAjaxController
 
     protected function dispatchInvestigateMutationsEvent(Report $report): InvestigateMutationsEvent
     {
-        $policy = $this->policyProvider->provideFor($report->scope);
+        // @todo for future versions, it might be considered to distinguish `enforce` and `report` in the database
+        $policy = $this->policyProvider->provideFor($report->scope, $report->details->resolveDisposition());
         $event = new InvestigateMutationsEvent($policy, $report);
         $this->eventDispatcher->dispatch($event);
         return $event;
diff --git a/typo3/sysext/backend/Classes/Security/ContentSecurityPolicy/CspModuleController.php b/typo3/sysext/backend/Classes/Security/ContentSecurityPolicy/CspModuleController.php
index cd1168e3b0e09e3df9cddd85e38567cae90ad9c3..7806e9fa60e455878554485001b73b3c0390a082 100644
--- a/typo3/sysext/backend/Classes/Security/ContentSecurityPolicy/CspModuleController.php
+++ b/typo3/sysext/backend/Classes/Security/ContentSecurityPolicy/CspModuleController.php
@@ -62,8 +62,11 @@ class CspModuleController
     {
         return [
             'featureDisabled' => array_filter([
-                'backend' => false,
-                'frontend' => !$this->features->isFeatureEnabled('security.frontend.enforceContentSecurityPolicy'),
+                'backend' => [],
+                'frontend' => !$this->features->isFeatureEnabled('security.frontend.enforceContentSecurityPolicy')
+                    && !$this->features->isFeatureEnabled('security.frontend.reportContentSecurityPolicy')
+                        ? ['enforce', 'report']
+                        : [],
             ]),
             'customReporting' => array_filter([
                 'BE' => $GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl'] ?? '',
diff --git a/typo3/sysext/backend/Resources/Private/Templates/Security/CspModule.html b/typo3/sysext/backend/Resources/Private/Templates/Security/CspModule.html
index 0be0a7d1133f76848ad46e08dcfea2660ffc1014..d303e5428288baed05c99e9d4019e99ede58f480 100644
--- a/typo3/sysext/backend/Resources/Private/Templates/Security/CspModule.html
+++ b/typo3/sysext/backend/Resources/Private/Templates/Security/CspModule.html
@@ -22,8 +22,10 @@
                 <f:translate key="LLL:EXT:backend/Resources/Private/Language/Modules/content-security-policy.xlf:module.callout.featureDisabled.message" />
             </p>
             <ul class="mb-0">
-                <f:for each="{configurationStatus.featureDisabled}" as="value" key="key">
-                    <li><code>security.{key}.enforceContentSecurityPolicy</code></li>
+                <f:for each="{configurationStatus.featureDisabled}" as="dispositions" key="scope">
+                    <f:for each="{dispositions}" as="disposition" iteration="iteration">
+                        <li><code>security.{scope}.{disposition}ContentSecurityPolicy</code></li>
+                    </f:for>
                 </f:for>
             </ul>
         </f:be.infobox>
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Configuration/DispositionConfiguration.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Configuration/DispositionConfiguration.php
new file mode 100644
index 0000000000000000000000000000000000000000..190290ecfe22f4c5b00f9cbb0a84f2b00465d7bd
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Configuration/DispositionConfiguration.php
@@ -0,0 +1,54 @@
+<?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\Configuration;
+
+/**
+ * Represents a `csp.yaml` site configuration section for the disposition modes `enforce` and `report.
+ *
+ * @internal
+ */
+readonly class DispositionConfiguration
+{
+    public function __construct(
+        public bool $inheritDefault,
+        public bool $includeResolutions,
+        public array $mutations = [],
+        /** @var array<string, bool> $packages */
+        public array $packages = [],
+    ) {}
+
+    public function resolveEffectivePackages(string ...$packageNames): array
+    {
+        if ($this->packages === []) {
+            return $packageNames;
+        }
+
+        $effectivePackageNames = [];
+        if (($this->packages['*'] ?? null) === true) {
+            $effectivePackageNames = $packageNames;
+        }
+
+        $dropPackageNames = array_filter($packageNames, fn(string $package): bool => ($this->packages[$package] ?? null) === false);
+        $effectivePackageNames = array_diff($effectivePackageNames, $dropPackageNames);
+
+        $includePackageNames = array_filter($packageNames, fn(string $package): bool => ($this->packages[$package] ?? null) === true);
+        $effectivePackageNames = [...$effectivePackageNames, ...array_diff($includePackageNames, $effectivePackageNames)];
+
+        return $effectivePackageNames;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Configuration/DispositionMapFactory.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Configuration/DispositionMapFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..e5cacee56340093cf8177ba1d432e60beb73c32f
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Configuration/DispositionMapFactory.php
@@ -0,0 +1,114 @@
+<?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\Configuration;
+
+use TYPO3\CMS\Core\Configuration\Features;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Disposition;
+use TYPO3\CMS\Core\Type\Map;
+
+/**
+ * Transforms a `csp.yaml` site configuration into a configuration model.
+ *
+ * @internal
+ */
+readonly class DispositionMapFactory
+{
+    public function __construct(private Features $features) {}
+
+    /**
+     * @return list<Disposition>
+     */
+    public function resolveFallbackDispositions(): array
+    {
+        $dispositions = [];
+        if ($this->features->isFeatureEnabled('security.frontend.enforceContentSecurityPolicy')) {
+            $dispositions[] = Disposition::enforce;
+        }
+        if ($this->features->isFeatureEnabled('security.frontend.reportContentSecurityPolicy')) {
+            $dispositions[] = Disposition::report;
+        }
+        return $dispositions;
+    }
+
+    /**
+     * @return Map<Disposition, DispositionConfiguration>
+     */
+    public function buildDispositionMap(array $siteConfiguration): Map
+    {
+        $activeAssignment = (bool)($siteConfiguration['active'] ?? true);
+        // @todo future TYPO3 v14 should explicitly require `active: true` to get rid of the feature fallbacks
+        if ($activeAssignment === false) {
+            return new Map();
+        }
+
+        $dispositions = new Map();
+        // assign site-specific dispositions
+        foreach (Disposition::cases() as $disposition) {
+            $assignment = $siteConfiguration[$disposition->value] ?? null;
+            if ($this->isActive($assignment)) {
+                $dispositions[$disposition] = $this->buildDispositionConfiguration(
+                    $assignment,
+                    $siteConfiguration
+                );
+            }
+        }
+        // in case there is no site-specific configuration, use the fallbacks as defined by top-level features
+        if (count($dispositions) === 0) {
+            foreach ($this->resolveFallbackDispositions() as $fallbackDisposition) {
+                // skip fallbacks in case the disposition was disabled explicitly (e.g. `enforce: false`)
+                if (($siteConfiguration[$fallbackDisposition->value] ?? null) !== false) {
+                    $dispositions[$fallbackDisposition] = $this->buildDispositionConfiguration(
+                        true,
+                        $siteConfiguration
+                    );
+                }
+            }
+        }
+        return $dispositions;
+    }
+
+    private function isActive(mixed $assignment): bool
+    {
+        return $assignment === true || is_array($assignment);
+    }
+
+    private function buildDispositionConfiguration(
+        true|array $assignment,
+        array $siteConfiguration = []
+    ): DispositionConfiguration {
+        if ($assignment === true) {
+            // take from top-level configuration
+            // (`includeResolutions` and `packages` are ignored on purpose)
+            $inheritDefault = $siteConfiguration['inheritDefault'] ?? true;
+            $includeResolutions = true;
+            $mutations = $siteConfiguration['mutations'] ?? [];
+            $packages = [];
+        } else {
+            $inheritDefault = $assignment['inheritDefault'] ?? true;
+            $includeResolutions = $assignment['includeResolutions'] ?? true;
+            $mutations = $assignment['mutations'] ?? [];
+            $packages = $assignment['packages'] ?? [];
+        }
+        return new DispositionConfiguration(
+            (bool)$inheritDefault,
+            (bool)$includeResolutions,
+            is_array($mutations) ? $mutations : [],
+            is_array($packages) ? $packages : [],
+        );
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Disposition.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Disposition.php
new file mode 100644
index 0000000000000000000000000000000000000000..9f0f5352aa7f9f0bcf40decf200e8478bdc89d9d
--- /dev/null
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Disposition.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 Content-Security-Policy disposition
+ * see https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP#disposition
+ */
+enum Disposition: string
+{
+    case enforce = 'enforce';
+    case report = 'report';
+
+    public function getHttpHeaderName(): string
+    {
+        return match ($this) {
+            self::enforce => 'Content-Security-Policy',
+            self::report => 'Content-Security-Policy-Report-Only',
+        };
+    }
+}
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationRepository.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationRepository.php
index 1466c8ff7905d65e7efdce3005b77e1d087cf49c..b48141b41d1a2328cd9a4139f805466e24d8cb5f 100644
--- a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationRepository.php
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/MutationRepository.php
@@ -18,6 +18,8 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy;
 
 use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\DispositionConfiguration;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\DispositionMapFactory;
 use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ResolutionRepository;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\SiteFinder;
@@ -28,6 +30,9 @@ use TYPO3\CMS\Core\Type\Map;
  */
 final class MutationRepository
 {
+    /**
+     * @var Map<Scope, Map<Disposition, Map<MutationOrigin, MutationCollection>>>
+     */
     private ?Map $resolvedMutations;
 
     /**
@@ -41,34 +46,32 @@ final class MutationRepository
         private readonly ModelService $modelService,
         private readonly ScopeRepository $scopeRepository,
         private readonly ResolutionRepository $resolutionRepository,
+        private readonly DispositionMapFactory $dispositionMapFactory,
     ) {
         $this->resolvedMutations = null;
     }
 
     /**
-     * @param bool $resolved whether to include resolved mutations (resolutions)
-     * @return Map<Scope, Map<MutationOrigin, MutationCollection>>
+     * @return Map<Scope, Map<Disposition, Map<MutationOrigin, MutationCollection>>>
      */
-    public function findAll(bool $resolved = true): Map
+    public function findAll(): Map
     {
-        if ($resolved) {
+        if ($this->resolvedMutations === null) {
             $this->resolveMutations();
-            return $this->resolvedMutations;
         }
-        return $this->staticMutations;
+        return $this->resolvedMutations;
     }
 
     /**
-     * @param bool $resolved whether to include resolved mutations (resolutions)
      * @return Map<MutationOrigin, MutationCollection>
      */
-    public function findByScope(Scope $scope, bool $resolved = true): Map
+    public function findByScope(Scope $scope, Disposition $disposition = Disposition::enforce): Map
     {
-        if ($resolved) {
+        if ($this->resolvedMutations === null) {
             $this->resolveMutations();
-            return $this->resolvedMutations[$scope] ?? new Map();
         }
-        return $this->staticMutations[$scope] ?? new Map();
+        $scope = $this->reduceScope($scope);
+        return $this->resolvedMutations[$scope][$disposition] ?? new Map();
     }
 
     private function resolveMutations(): void
@@ -76,58 +79,127 @@ final class MutationRepository
         if ($this->resolvedMutations !== null) {
             return;
         }
-        $this->resolvedMutations = clone $this->staticMutations;
+
+        $this->resolvedMutations = new Map();
         $allScopes = $this->scopeRepository->findAll();
-        // fetch resolution mutations from the database
+        // fetch resolutions from the database & assign them later to the resolved mutations map
+        $resolutions = new Map();
         foreach ($this->resolutionRepository->findAll() as $resolution) {
             // only for existing scopes (e.g. ignore scopes for sites, that are not existing anymore)
             if (in_array($resolution->scope, $allScopes, true)) {
                 $mutationOrigin = new MutationOrigin(MutationOriginType::resolution, $resolution->summary);
-                $target = $this->provideScopeInResolvedMutations($resolution->scope);
-                $target[$mutationOrigin] = $resolution->mutationCollection;
+                $scopedTarget = $this->provideScopeInMap($resolution->scope, $resolutions);
+                $scopedTarget[$mutationOrigin] = $resolution->mutationCollection;
+            }
+        }
+        // assign generic backend and frontend scopes
+        foreach ([Scope::backend(), Scope::frontend()] as $scope) {
+            $scopedTarget = $this->provideScopeInMap($scope, $this->resolvedMutations);
+            $dispositions = $scope === Scope::frontend()
+                ? $this->dispositionMapFactory->resolveFallbackDispositions()
+                : [Disposition::enforce];
+            foreach ($dispositions as $disposition) {
+                $disposedTarget = $this->provideDispositionInMap($disposition, $scopedTarget);
+                if (isset($this->staticMutations[$scope])) {
+                    $disposedTarget->assign($this->staticMutations[$scope]);
+                }
+                if (isset($resolutions[$scope])) {
+                    $disposedTarget->assign($resolutions[$scope]);
+                }
             }
         }
-        // fetch site-specific mutations
+        // fetch and assign site-specific mutations
         foreach ($this->scopeRepository->findAllFrontendSites() as $scope) {
             $site = $this->resolveSite($scope);
-            $target = $this->provideScopeInResolvedMutations($scope);
-            $shallInheritDefault = (bool)($site->getConfiguration()['contentSecurityPolicies']['inheritDefault'] ?? true);
-            if ($shallInheritDefault && $scope->isFrontendSite()) {
-                foreach ($this->resolvedMutations[Scope::frontend()] ?? [] as $existingOrigin => $existingCollection) {
-                    $target[$existingOrigin] = clone $existingCollection;
+            $scopedTarget = $this->provideScopeInMap($scope, $this->resolvedMutations);
+            // fetch site-specific `enforce` and/or `report` disposition configuration
+            $dispositionMap = $this->dispositionMapFactory->buildDispositionMap(
+                $site->getConfiguration()['contentSecurityPolicies']
+            );
+            /**
+             * @var Disposition $disposition
+             * @var DispositionConfiguration $dispositionConfiguration
+             */
+            foreach ($dispositionMap as $disposition => $dispositionConfiguration) {
+                $disposedTarget = $this->provideDispositionInMap($disposition, $scopedTarget);
+                $disposedTarget->assign($this->resolveStaticMutations($scope, $dispositionConfiguration));
+                if ($dispositionConfiguration->includeResolutions && isset($resolutions[$scope])) {
+                    $disposedTarget->assign($resolutions[$scope]);
+                }
+                $mutationCollection = $this->resolveFrontendSiteMutationCollection($dispositionConfiguration);
+                if ($mutationCollection !== null) {
+                    $mutationOrigin = new MutationOrigin(MutationOriginType::site, $scope->siteIdentifier);
+                    $disposedTarget[$mutationOrigin] = $mutationCollection;
                 }
             }
-            $mutationCollection = $this->resolveFrontendSiteMutationCollection($site);
-            if ($mutationCollection !== null) {
-                $mutationOrigin = new MutationOrigin(MutationOriginType::site, $scope->siteIdentifier);
-                $target[$mutationOrigin] = $mutationCollection;
+        }
+    }
+
+    /**
+     * Resolves site-specific static mutations, applies `inheritDefault` configuration
+     * and filters generic static mutations based on the `packages` configuration.
+     *
+     * @return Map<MutationOrigin, MutationCollection>
+     */
+    private function resolveStaticMutations(Scope $scope, DispositionConfiguration $dispositionConfiguration): Map
+    {
+        $target = new Map();
+        $scope = $this->reduceScope($scope);
+
+        if ($dispositionConfiguration->inheritDefault && isset($this->staticMutations[Scope::frontend()])) {
+            // mutations from `ContentSecurityPolicies.php` for generic frontend scope
+            $target->assign($this->staticMutations[Scope::frontend()]);
+        }
+        // mutations from `ContentSecurityPolicies.php` for a specific site identifier
+        if (isset($this->staticMutations[$scope])) {
+            $target->assign($this->staticMutations[$scope]);
+        }
+
+        // filter mutation origins by effective package names
+        $packageOrigins = array_filter(
+            $target->keys(),
+            static fn(MutationOrigin $origin) => $origin->type === MutationOriginType::package
+        );
+        $packageNames = array_map(static fn(MutationOrigin $origin) => $origin->value, $packageOrigins);
+        $effectivePackageNames = $dispositionConfiguration->resolveEffectivePackages(...$packageNames);
+        foreach ($packageOrigins as $mutationOrigin) {
+            if (!in_array($mutationOrigin->value, $effectivePackageNames, true)) {
+                unset($target[$mutationOrigin]);
             }
         }
+        return $target;
     }
 
-    private function resolveFrontendSiteMutationCollection(Site $site): ?MutationCollection
+    private function resolveFrontendSiteMutationCollection(DispositionConfiguration $dispositionConfiguration): ?MutationCollection
     {
-        $mutationConfigurations = $site->getConfiguration()['contentSecurityPolicies']['mutations'] ?? [];
-        if (empty($mutationConfigurations) || !is_array($mutationConfigurations)) {
+        if ($dispositionConfiguration->mutations === []) {
             return null;
         }
         $mutations = array_map(
             fn(array $array) => $this->modelService->buildMutationFromArray($array),
-            $mutationConfigurations
+            $dispositionConfiguration->mutations
         );
         return new MutationCollection(...$mutations);
     }
 
+    private function provideDispositionInMap(Disposition $disposition, Map $map): Map
+    {
+        if (!isset($map[$disposition])) {
+            $map[$disposition] = new Map();
+        }
+        return $map[$disposition];
+    }
+
     /**
      * @return Map<MutationOrigin, MutationCollection>
      */
-    private function provideScopeInResolvedMutations(Scope $scope): Map
+    private function provideScopeInMap(Scope $scope, Map $map): Map
     {
         $reducedScope = $this->reduceScope($scope);
-        if (!isset($this->resolvedMutations[$reducedScope])) {
-            $this->resolvedMutations[$reducedScope] = new Map();
+        if (!isset($map[$reducedScope])) {
+            $map[$reducedScope] = new Map();
         }
-        return $this->resolvedMutations[$reducedScope];
+        return $map[$reducedScope];
     }
 
     /**
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/PolicyProvider.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/PolicyProvider.php
index f8fd9f51c9fc7547a6dbd1442f3f39e79caa9bb0..9d0285038fd8b8364cbc943fac3aab2f700e472a 100644
--- a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/PolicyProvider.php
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/PolicyProvider.php
@@ -52,12 +52,15 @@ final class PolicyProvider
     /**
      * Provides the complete, dynamically mutated policy to be used in HTTP responses.
      */
-    public function provideFor(Scope $scope, ?ServerRequestInterface $request = null): Policy
-    {
+    public function provideFor(
+        Scope $scope,
+        Disposition $disposition = Disposition::enforce,
+        ?ServerRequestInterface $request = null,
+    ): Policy {
         // @todo add policy cache per scope
         $defaultPolicy = new Policy();
         $mutationCollections = iterator_to_array(
-            $this->mutationRepository->findByScope($scope),
+            $this->mutationRepository->findByScope($scope, $disposition),
             false
         );
         // add temporary(!) mutations that were collected during processing this request
diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Reporting/ReportDetails.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Reporting/ReportDetails.php
index 3b619d9cd9961522a9c90442acbd693cd88e35f7..63653184c428e126703348afbca56e0f87da89a0 100644
--- a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Reporting/ReportDetails.php
+++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/Reporting/ReportDetails.php
@@ -17,6 +17,8 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting;
 
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Disposition;
+
 /**
  * @internal
  */
@@ -39,6 +41,11 @@ class ReportDetails extends \ArrayObject implements \JsonSerializable
         );
     }
 
+    public function resolveDisposition(): Disposition
+    {
+        return Disposition::tryFrom($this['disposition'] ?? '') ?? Disposition::enforce;
+    }
+
     protected static function toCamelCase(string $value): string
     {
         return lcfirst(str_replace('-', '', ucwords($value, '-')));
diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php
index 4a5b057b0a57b7e98aa0198a2cf3de9f62772ebb..712dc2496108a7a1d8652fd7ef35bc65cb8bba51 100644
--- a/typo3/sysext/core/Configuration/DefaultConfiguration.php
+++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php
@@ -85,6 +85,7 @@ return [
             'security.backend.htmlSanitizeRte' => false,
             'security.backend.enforceReferrer' => true,
             'security.frontend.enforceContentSecurityPolicy' => false,
+            'security.frontend.reportContentSecurityPolicy' => false,
             'security.frontend.allowInsecureSiteResolutionByQueryParameters' => false,
             'security.frontend.allowInsecureFrameOptionInShowImageController' => false,
         ],
diff --git a/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml
index 9e7021d19eeb56dbc33f46d2e4019fc5d75d3b5f..462805f4966e155099e4581efd8ba24c63303d67 100644
--- a/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml
+++ b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml
@@ -195,7 +195,10 @@ SYS:
                   required `Referer` header. As this is a potential security risk, it is recommended to enable this option.'
               security.frontend.enforceContentSecurityPolicy:
                 type: bool
-                description: 'If on, HTTP Content-Security-Policy header will be applied for each HTTP frontend request.'
+                description: 'If on, HTTP Content-Security-Policy header will be applied for each HTTP frontend request. This can be overruled using a csp.yaml site configuration.'
+              security.frontend.reportContentSecurityPolicy:
+                type: bool
+                description: 'If on, HTTP Content-Security-Policy-Report-Only header will be applied for each HTTP frontend request. This can be overruled using a csp.yaml site configuration.'
               security.frontend.allowInsecureFrameOptionInShowImageController:
                 type: bool
                 description: 'If on, the eID Script "tx_cms_showpic" respects the GET parameter "frame" without being signed. Should not be enabled as this allows uncontrolled resource consumption.'
diff --git a/typo3/sysext/core/Documentation/Changelog/12.4.x/Important-101580-IntroduceContentSecurityPolicyReportOnlyHandling.rst b/typo3/sysext/core/Documentation/Changelog/12.4.x/Important-101580-IntroduceContentSecurityPolicyReportOnlyHandling.rst
new file mode 100644
index 0000000000000000000000000000000000000000..73816552700a254fdab4f2cc923e41e1b1a501eb
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.4.x/Important-101580-IntroduceContentSecurityPolicyReportOnlyHandling.rst
@@ -0,0 +1,29 @@
+.. include:: /Includes.rst.txt
+
+.. _important-101580-1723653576:
+
+===========================================================================
+Important: #101580 - Introduce Content-Security-Policy-Report-Only handling
+===========================================================================
+
+See :issue:`101580`
+
+Description
+===========
+
+The feature flag `security.frontend.reportContentSecurityPolicy`
+(:php:`$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.reportContentSecurityPolicy']`)
+can be used to apply the `Content-Security-Policy-Report-Only` HTTP header for
+frontend responses.
+
+When both feature flags are activated, both headers are sent.
+You can deactivate one disposition in the site-specific configuration.
+
+This allows to test and assess the potential impact on introducing
+Content-Security-Policy in the frontend - without actually blocking
+any functionality.
+
+This behavior can be controlled on a site-specific scope as well, see
+:ref:`Important: #104549 - Introduce site-specific Content-Security-Policy-Disposition <important-104549-1723461851>`.
+
+.. index:: Frontend, LocalConfiguration, ext:frontend
diff --git a/typo3/sysext/core/Documentation/Changelog/12.4.x/Important-104549-IntroduceSiteSpecificContentSecurityPolicyDisposition.rst b/typo3/sysext/core/Documentation/Changelog/12.4.x/Important-104549-IntroduceSiteSpecificContentSecurityPolicyDisposition.rst
new file mode 100644
index 0000000000000000000000000000000000000000..c4ee46c721323bb4fbc89e1bbba8f4ab7f84c9c4
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.4.x/Important-104549-IntroduceSiteSpecificContentSecurityPolicyDisposition.rst
@@ -0,0 +1,135 @@
+.. include:: /Includes.rst.txt
+
+.. _important-104549-1723461851:
+
+================================================================================
+Important: #104549 - Introduce site-specific Content-Security-Policy-Disposition
+================================================================================
+
+See :issue:`104549`
+
+Description
+===========
+
+The feature flags :php:`$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.enforceContentSecurityPolicy']`
+and :php:`$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.reportContentSecurityPolicy']` apply
+Content-Security-Policy headers to any frontend site. The dedicated :file:`sites/<my-site>/csp.yaml` can now be
+used as alternative to declare the desired disposition of `Content-Security-Policy` and
+`Content-Security-Policy-Report-Only` individually.
+
+It now is also possible, to apply both `Content-Security-Policy` and `Content-Security-Policy-Report-Only`
+HTTP headers at the same time with different directives for a particular site. Besides that it is possible
+to disable the disposition completely for a site.
+
+The following new configuration schemes were introduced for :file:`sites/<my-site>/csp.yaml`:
+
+* `active (false)` for disabling CSP for a particular site, which overrules any other setting for `enforce` or `report`
+* `enforce (bool|disposition-array)` for compiling the `Content-Security-Policy` HTTP header
+* `report (bool|disposition-array)` for compiling the `Content-Security-Policy-Report-Only` HTTP header
+
+The `disposition-array` for `enforce` and `report` allows these properties:
+
+* `inheritDefault (bool)` inherits default site-unspecific frontend policy mutations (`true` per default)
+* `includeResolutions (bool)` includes dynamic resolutions, as persisted in the database via backend module (`true` per default)
+* `mutations (mutation-item-array)` defines additional directive mutations to be applied to the specific site
+* `packages (package-item-array)` defines packages/extensions whose static CSP mutations shall be dropped or included
+
+Example: Disable Content-Security-Policy
+----------------------------------------
+
+The following example would completely disable CSP for a particular site.
+
+..  code-block:: yaml
+    :caption: config/sites/<my-site>/csp.yaml
+
+    # `active` is enabled per default if omitted
+    active: false
+
+Example: Use `report` disposition
+---------------------------------
+
+The following example would dispose only `Content-Security-Policy-Report-Only`
+for a particular site (since the `enforce` property is not given).
+
+..  code-block:: yaml
+    :caption: config/sites/<my-site>/csp.yaml
+
+    report:
+      # `inheritDefault` is enabled per default if omitted
+      inheritDefault: true
+      mutations:
+       - mode: extend
+         directive: img-src
+         sources:
+          - https://*.typo3.org
+
+The following example is equivalent to the previous, but shows that the
+legacy configuration (having `inheritDefault` and `mutations` on the top-level)
+is still supported.
+
+The effective HTTP headers would then be resolved from the active feature flags
+`security.frontend.enforceContentSecurityPolicy` and
+`security.frontend.reportContentSecurityPolicy` - in case both flags are active,
+both HTTP headers `Content-Security-Policy` and `Content-Security-Policy-Read-Only`
+would be used.
+
+..  code-block:: yaml
+    :caption: config/sites/<my-site>/csp.yaml
+
+    # `inheritDefault` is enabled per default if omitted
+    inheritDefault: true
+    mutations:
+     - mode: extend
+       directive: img-src
+       sources:
+        - https://*.typo3.org
+
+Example: Use `enforce` and `report` dispositions at the same time
+-----------------------------------------------------------------
+
+The following example would dispose `Content-Security-Policy` (`enforce`)
+and `Content-Security-Policy-Report-Only` (`report`) for a particular site.
+
+This allows to test new CSP directives in the frontend - the example drops
+the static CSP directives of the package `my-vendor/my-package` in the
+enforced disposition and only applies it to the reporting disposition.
+
+..  code-block:: yaml
+    :caption: config/sites/<my-site>/csp.yaml
+
+    enforce:
+      # `inheritDefault` is enabled per default if omitted
+      inheritDefault: true
+      # `includeResolutions` is enabled per default if omitted
+      includeResolutions: true
+      mutations:
+        - mode: extend
+          directive: img-src
+          sources:
+            - https://*.typo3.org
+      packages:
+        # all (`*`) packages shall be included (`true`)
+        '*': true
+        # the package `my-vendor/my-package` shall be dropped (`false`)
+        my-vendor/my-package: false
+
+    report:
+      # `inheritDefault` is enabled per default if omitted
+      inheritDefault: true
+      # `includeResolutions` is enabled per default if omitted
+      includeResolutions: true
+      mutations:
+        - mode: extend
+          directive: img-src
+          sources:
+            - https://*.my-vendor.example.org/
+      # the `packages` section can be omitted in this case, since all packages
+      # listed there shall be included - which is the default behavior in case
+      # `packages` would not be configured
+      packages:
+        # all (`*`) packages shall be included (`true`)
+        '*': true
+        # the package `my-vendor/my-package` shall be dropped (`false`)
+        my-vendor/my-package: true
+
+.. index:: Frontend, YAML, ext:frontend
diff --git a/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/Configuration/DispositionConfigurationTest.php b/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/Configuration/DispositionConfigurationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a08e666be4a2a096836cb99235ba4e45c0ba72e3
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/Configuration/DispositionConfigurationTest.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\Tests\Unit\Security\ContentSecurityPolicy\Configuration;
+
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\DispositionConfiguration;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+final class DispositionConfigurationTest extends UnitTestCase
+{
+    public static function effectivePackagesAreResolvedDataProvider(): \Generator
+    {
+        yield 'empty packages' => [
+            'packages' => [],
+            'packageNames' => ['a/a', 'b/b', 'c/c'],
+            'expectation' => ['a/a', 'b/b', 'c/c'],
+        ];
+        yield 'missing definition for `*`' => [
+            'packages' => [
+                // missing here: '*' => true,
+                'b/b' => true,
+            ],
+            'packageNames' => ['a/a', 'b/b', 'c/c'],
+            'expectation' => ['b/b'],
+        ];
+        yield 'all to be included' => [
+            'packages' => [
+                '*' => true,
+            ],
+            'packageNames' => ['a/a', 'b/b', 'c/c'],
+            'expectation' => ['a/a', 'b/b', 'c/c'],
+        ];
+        yield 'all to be included, b/b & c/c dropped' => [
+            'packages' => [
+                '*' => true,
+                'b/b' => false,
+                'c/c' => false,
+            ],
+            'packageNames' => ['a/a', 'b/b', 'c/c'],
+            'expectation' => ['a/a'],
+        ];
+        yield 'all to be included, ignoring configured non/existing' => [
+            'packages' => [
+                '*' => true,
+                'non/existing' => true,
+            ],
+            'packageNames' => ['a/a', 'b/b', 'c/c'],
+            'expectation' => ['a/a', 'b/b', 'c/c'],
+        ];
+        yield 'all to be dropped' => [
+            'packages' => [
+                '*' => false,
+            ],
+            'packageNames' => ['a/a', 'b/b', 'c/c'],
+            'expectation' => [],
+        ];
+        yield 'all to be dropped, b/b & c/c included' => [
+            'packages' => [
+                '*' => false,
+                'b/b' => true,
+                'c/c' => true,
+            ],
+            'packageNames' => ['a/a', 'b/b', 'c/c'],
+            'expectation' => ['b/b', 'c/c'],
+        ];
+    }
+
+    #[DataProvider('effectivePackagesAreResolvedDataProvider')]
+    #[Test]
+    public function effectivePackagesAreResolved(array $packages, array $packageNames, array $expectation): void
+    {
+        $subject = new DispositionConfiguration(true, true, [], $packages);
+        self::assertSame($expectation, $subject->resolveEffectivePackages(...$packageNames));
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/Configuration/DispositionMapFactoryTest.php b/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/Configuration/DispositionMapFactoryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0f9021d039abeb971bf6837a6b6da6e627013580
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Security/ContentSecurityPolicy/Configuration/DispositionMapFactoryTest.php
@@ -0,0 +1,281 @@
+<?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\Configuration;
+
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
+use TYPO3\CMS\Core\Configuration\Features;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\DispositionConfiguration;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\DispositionMapFactory;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Disposition;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+final class DispositionMapFactoryTest extends UnitTestCase
+{
+    public static function featuresAreReflectedInDispositionMapDataProvider(): \Generator
+    {
+        yield 'all features disabled' => [
+            'features' => [
+                'security.frontend.enforceContentSecurityPolicy' => false,
+                'security.frontend.reportContentSecurityPolicy' => false,
+            ],
+            'expectation' => [],
+        ];
+        yield 'enforce feature enabled' => [
+            'features' => [
+                'security.frontend.enforceContentSecurityPolicy' => true,
+                'security.frontend.reportContentSecurityPolicy' => false,
+            ],
+            'expectation' => [Disposition::enforce],
+        ];
+        yield 'report feature enabled' => [
+            'features' => [
+                'security.frontend.enforceContentSecurityPolicy' => false,
+                'security.frontend.reportContentSecurityPolicy' => true,
+            ],
+            'expectation' => [Disposition::report],
+        ];
+        yield 'both features enabled' => [
+            'features' => [
+                'security.frontend.enforceContentSecurityPolicy' => true,
+                'security.frontend.reportContentSecurityPolicy' => true,
+            ],
+            'expectation' => [Disposition::enforce, Disposition::report],
+        ];
+    }
+
+    #[DataProvider('featuresAreReflectedInDispositionMapDataProvider')]
+    #[Test]
+    public function featuresAreReflectedInDispositionMap(array $features, array $expectation): void
+    {
+        $featuresMock = $this->createFeaturesMock($features);
+        $subject = new DispositionMapFactory($featuresMock);
+        $result = $subject->buildDispositionMap([]);
+        self::assertSame($expectation, $result->keys());
+    }
+
+    public static function configurationIsReflectedInDispositionMapDataProvider(): \Generator
+    {
+        $allFeaturesDisabled = [
+            'security.frontend.enforceContentSecurityPolicy' => false,
+            'security.frontend.reportContentSecurityPolicy' => false,
+        ];
+        $bothFeaturesEnabled = [
+            'security.frontend.enforceContentSecurityPolicy' => true,
+            'security.frontend.reportContentSecurityPolicy' => true,
+        ];
+
+        yield 'all features disabled, active:false' => [
+            'features' => $allFeaturesDisabled,
+            'configuration' => ['active' => false],
+            'expectation' => [],
+        ];
+        yield 'both features enabled, active:false' => [
+            'features' => $bothFeaturesEnabled,
+            'configuration' => ['active' => false],
+            'expectation' => [],
+        ];
+        // @todo `active: true` for explicitly activating CSP is not in effect yet (v14 topic)
+        yield 'all features disabled, active:true' => [
+            'features' => $allFeaturesDisabled,
+            'configuration' => ['active' => true],
+            'expectation' => [],
+        ];
+        // @todo `active: true` for explicitly activating CSP is not in effect yet (v14 topic)
+        yield 'both features enabled, active:true' => [
+            'features' => $bothFeaturesEnabled,
+            'configuration' => ['active' => true],
+            'expectation' => [Disposition::enforce, Disposition::report],
+        ];
+        yield 'all features disabled, enforce:false' => [
+            'features' => $allFeaturesDisabled,
+            'configuration' => ['enforce' => false],
+            'expectation' => [],
+        ];
+        yield 'both features enabled, enforce:false' => [
+            'features' => $bothFeaturesEnabled,
+            'configuration' => ['enforce' => false],
+            'expectation' => [Disposition::report],
+        ];
+        yield 'all features disabled, enforce:true' => [
+            'features' => $allFeaturesDisabled,
+            'configuration' => ['enforce' => true],
+            'expectation' => [Disposition::enforce],
+        ];
+        yield 'both features enabled, enforce:true' => [
+            'features' => $bothFeaturesEnabled,
+            'configuration' => ['enforce' => true],
+            'expectation' => [Disposition::enforce],
+        ];
+        yield 'all features disabled, enforce:array' => [
+            'features' => $allFeaturesDisabled,
+            'configuration' => ['enforce' => []],
+            'expectation' => [Disposition::enforce],
+        ];
+        yield 'both features enabled, enforce:array' => [
+            'features' => $bothFeaturesEnabled,
+            'configuration' => ['enforce' => []],
+            'expectation' => [Disposition::enforce],
+        ];
+        yield 'all features disabled, report:false' => [
+            'features' => $allFeaturesDisabled,
+            'configuration' => ['report' => false],
+            'expectation' => [],
+        ];
+        yield 'both features enabled, report:false' => [
+            'features' => $bothFeaturesEnabled,
+            'configuration' => ['report' => false],
+            'expectation' => [Disposition::enforce],
+        ];
+        yield 'all features disabled, report:true' => [
+            'features' => $allFeaturesDisabled,
+            'configuration' => ['report' => true],
+            'expectation' => [Disposition::report],
+        ];
+        yield 'both features enabled, report:true' => [
+            'features' => $bothFeaturesEnabled,
+            'configuration' => ['report' => true],
+            'expectation' => [Disposition::report],
+        ];
+        yield 'all features disabled, report:array' => [
+            'features' => $allFeaturesDisabled,
+            'configuration' => ['report' => []],
+            'expectation' => [Disposition::report],
+        ];
+        yield 'both features enabled, report:array' => [
+            'features' => $bothFeaturesEnabled,
+            'configuration' => ['report' => []],
+            'expectation' => [Disposition::report],
+        ];
+        yield 'all features disabled, enforce:false & report:false' => [
+            'features' => $allFeaturesDisabled,
+            'configuration' => ['enforce' => false, 'report' => false],
+            'expectation' => [],
+        ];
+        yield 'both features enabled, enforce:false & report:false' => [
+            'features' => $bothFeaturesEnabled,
+            'configuration' => ['enforce' => false, 'report' => false],
+            'expectation' => [],
+        ];
+        yield 'all features disabled, enforce:true & report:true' => [
+            'features' => $allFeaturesDisabled,
+            'configuration' => ['enforce' => true, 'report' => true],
+            'expectation' => [Disposition::enforce, Disposition::report],
+        ];
+        yield 'both features enabled, enforce:true & report:true' => [
+            'features' => $bothFeaturesEnabled,
+            'configuration' => ['enforce' => true, 'report' => true],
+            'expectation' => [Disposition::enforce, Disposition::report],
+        ];
+        yield 'all features disabled, enforce:array & report:array' => [
+            'features' => $allFeaturesDisabled,
+            'configuration' => ['enforce' => [], 'report' => []],
+            'expectation' => [Disposition::enforce, Disposition::report],
+        ];
+        yield 'both features enabled, enforce:array & report:array' => [
+            'features' => $bothFeaturesEnabled,
+            'configuration' => ['enforce' => [], 'report' => []],
+            'expectation' => [Disposition::enforce, Disposition::report],
+        ];
+    }
+
+    #[DataProvider('configurationIsReflectedInDispositionMapDataProvider')]
+    #[Test]
+    public function configurationIsReflectedInDispositionMap(array $features, array $configuration, array $expectation): void
+    {
+        $featuresMock = $this->createFeaturesMock($features);
+        $subject = new DispositionMapFactory($featuresMock);
+        $result = $subject->buildDispositionMap($configuration);
+        self::assertSame($expectation, $result->keys());
+    }
+
+    #[Test]
+    public function enforceConfigurationIsReflectedInEnforceDispositionConfiguration(): void
+    {
+        // note: `security.frontend.enforceContentSecurityPolicy` is enabled per default for this test
+        $featuresMock = $this->createFeaturesMock();
+        $subject = new DispositionMapFactory($featuresMock);
+
+        $enforceConfiguration = [
+            'inheritDefault' => false,
+            'includeResolutions' => false,
+            'mutations' => [
+                [
+                    'mode' => 'extend',
+                    'directive' => Directive::ImgSrc,
+                    'sources' => [],
+                ],
+            ],
+            'packages' => [
+                'my-vendor/my-package' => true,
+            ],
+        ];
+        $result = $subject->buildDispositionMap(['enforce' => $enforceConfiguration]);
+
+        self::assertInstanceOf(DispositionConfiguration::class, $result[Disposition::enforce]);
+        self::assertSame($enforceConfiguration['inheritDefault'], $result[Disposition::enforce]->inheritDefault);
+        self::assertSame($enforceConfiguration['includeResolutions'], $result[Disposition::enforce]->includeResolutions);
+        self::assertSame($enforceConfiguration['mutations'], $result[Disposition::enforce]->mutations);
+        self::assertSame($enforceConfiguration['packages'], $result[Disposition::enforce]->packages);
+    }
+
+    #[Test]
+    public function legacyTopLevelConfigurationIsReflectedInEnforceDispositionConfiguration(): void
+    {
+        // note: `security.frontend.enforceContentSecurityPolicy` is enabled per default for this test
+        $featuresMock = $this->createFeaturesMock();
+        $subject = new DispositionMapFactory($featuresMock);
+
+        $topLevelConfiguration = [
+            'inheritDefault' => false,
+            // `includeResolutions` will be ignored in top-level configuration
+            'includeResolutions' => false,
+            'mutations' => [
+                [
+                    'mode' => 'extend',
+                    'directive' => Directive::ImgSrc,
+                    'sources' => [],
+                ],
+            ],
+            // `packages` will be ignored in top-level configuration
+            'packages' => [
+                'my-vendor/my-package' => true,
+            ],
+        ];
+        $result = $subject->buildDispositionMap($topLevelConfiguration);
+
+        self::assertInstanceOf(DispositionConfiguration::class, $result[Disposition::enforce]);
+        self::assertSame($topLevelConfiguration['inheritDefault'], $result[Disposition::enforce]->inheritDefault);
+        // `includeResolutions` is ignored in top-level configuration
+        self::assertTrue($result[Disposition::enforce]->includeResolutions);
+        self::assertSame($topLevelConfiguration['mutations'], $result[Disposition::enforce]->mutations);
+        // `packages` are ignored in top-level configuration
+        self::assertEmpty($result[Disposition::enforce]->packages);
+    }
+
+    private function createFeaturesMock(?array $features = null): Features
+    {
+        $features ??= ['security.frontend.enforceContentSecurityPolicy' => true];
+        $featuresMock = $this->createMock(Features::class);
+        $featuresMock->method('isFeatureEnabled')->willReturnCallback(
+            static fn(string $featureName) => !empty($features[$featureName])
+        );
+        return $featuresMock;
+    }
+}
diff --git a/typo3/sysext/frontend/Classes/Middleware/ContentSecurityPolicyHeaders.php b/typo3/sysext/frontend/Classes/Middleware/ContentSecurityPolicyHeaders.php
index c80aed7fbb1ec2c02a0607015fdac207dd4499b7..329b3da7e22915ff8ba97e2dad74f39ca4fc10b3 100644
--- a/typo3/sysext/frontend/Classes/Middleware/ContentSecurityPolicyHeaders.php
+++ b/typo3/sysext/frontend/Classes/Middleware/ContentSecurityPolicyHeaders.php
@@ -24,11 +24,12 @@ use Psr\Http\Server\RequestHandlerInterface;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\Attribute\Autowire;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
-use TYPO3\CMS\Core\Configuration\Features;
 use TYPO3\CMS\Core\Core\RequestId;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\DispositionMapFactory;
 use TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyProvider;
 use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
 use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
+use TYPO3\CMS\Core\Site\Entity\Site;
 
 /**
  * Adds Content-Security-Policy headers to response.
@@ -38,25 +39,28 @@ use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
 final readonly class ContentSecurityPolicyHeaders implements MiddlewareInterface
 {
     public function __construct(
-        private Features $features,
         private RequestId $requestId,
         private LoggerInterface $logger,
         #[Autowire(service: 'cache.assets')]
         private FrontendInterface $cache,
         private PolicyProvider $policyProvider,
+        private DispositionMapFactory $dispositionMapFactory,
     ) {}
 
     public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
     {
+        $site = $request->getAttribute('site');
+        $dispositionMap = $this->dispositionMapFactory->buildDispositionMap(
+            $site instanceof Site ? $site->getConfiguration()['contentSecurityPolicies'] : []
+        );
         // return early in case CSP shall not be used
-        if (!$this->features->isFeatureEnabled('security.frontend.enforceContentSecurityPolicy')) {
+        if ($dispositionMap->keys() === []) {
             return $handler->handle($request);
         }
         // make sure, the nonce value is set before processing the remaining middlewares
         $request = $request->withAttribute('nonce', $this->requestId->nonce);
         $response = $handler->handle($request);
 
-        $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', [
@@ -66,14 +70,20 @@ final readonly class ContentSecurityPolicyHeaders implements MiddlewareInterface
             return $response;
         }
 
-        $policy = $this->policyProvider->provideFor($scope, $request);
-        if ($policy->isEmpty()) {
-            return $response;
-        }
-        $reportingUri = $this->policyProvider->getReportingUrlFor($scope, $request);
-        if ($reportingUri !== null) {
-            $policy = $policy->report(UriValue::fromUri($reportingUri));
+        foreach ($dispositionMap->keys() as $disposition) {
+            $policy = $this->policyProvider->provideFor($scope, $disposition, $request);
+            if ($policy->isEmpty()) {
+                continue;
+            }
+            $reportingUri = $this->policyProvider->getReportingUrlFor($scope, $request);
+            if ($reportingUri !== null) {
+                $policy = $policy->report(UriValue::fromUri($reportingUri));
+            }
+            $response = $response->withHeader(
+                $disposition->getHttpHeaderName(),
+                $policy->compile($this->requestId->nonce, $this->cache)
+            );
         }
-        return $response->withHeader('Content-Security-Policy', $policy->compile($this->requestId->nonce, $this->cache));
+        return $response;
     }
 }
diff --git a/typo3/sysext/lowlevel/Classes/ConfigurationModuleProvider/ContentSecurityPolicyMutationsProvider.php b/typo3/sysext/lowlevel/Classes/ConfigurationModuleProvider/ContentSecurityPolicyMutationsProvider.php
index 19897d88511c611e5b9e9f053b55ec8bc7845bed..537206e5d095539e35d0b818f180de4b5884c42f 100644
--- a/typo3/sysext/lowlevel/Classes/ConfigurationModuleProvider/ContentSecurityPolicyMutationsProvider.php
+++ b/typo3/sysext/lowlevel/Classes/ConfigurationModuleProvider/ContentSecurityPolicyMutationsProvider.php
@@ -18,6 +18,7 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Lowlevel\ConfigurationModuleProvider;
 
 use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce;
+use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Disposition;
 use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ModelService;
 use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
 use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationOrigin;
@@ -42,40 +43,48 @@ class ContentSecurityPolicyMutationsProvider extends AbstractProvider
         $data = [];
         /**
          * @var Scope $scope
-         * @var Map<MutationOrigin, MutationCollection> $scopeDetails
+         * @var Map<Disposition, Map<MutationOrigin, MutationCollection>> $scopeDetails
          */
         foreach ($this->mutationRepository->findAll() as $scope => $scopeDetails) {
             $policy = new Policy();
             $scopeValue = (string)$scope;
             $data[$scopeValue] = [];
-            /**
-             * @var MutationOrigin $mutationOrigin
-             * @var MutationCollection $mutationCollection
-             */
-            foreach ($scopeDetails as $mutationOrigin => $mutationCollection) {
-                $policy->mutate($mutationCollection);
-                $mutationOriginValue = sprintf(
-                    "%s '%s'",
-                    $mutationOrigin->type->value,
-                    $mutationOrigin->value
-                );
-                foreach ($mutationCollection->mutations as $mutation) {
-                    $sourceValues = array_map(
-                        // like `ModelService::compileSources()`, but for a single item & without a nonce
-                        fn(SourceInterface $source) => $source instanceof SourceValueInterface
-                            ? $source->compile()
-                            : $this->modelService->serializeSource($source),
-                        $mutation->sources
-                    );
-                    $data[$scopeValue][$mutationOriginValue][] = sprintf(
-                        '%s: %s %s',
-                        $mutation->mode->value,
-                        $mutation->directive->value,
-                        implode(' ', $sourceValues)
+
+            foreach ($scopeDetails as $disposition => $dispositionDetails) {
+                $data[$scopeValue][$disposition->value] = [];
+
+                /**
+                 * @var MutationOrigin $mutationOrigin
+                 * @var MutationCollection $mutationCollection
+                 */
+                foreach ($dispositionDetails as $mutationOrigin => $mutationCollection) {
+                    $policy->mutate($mutationCollection);
+                    $mutationOriginValue = sprintf(
+                        "%s '%s'",
+                        $mutationOrigin->type->value,
+                        $mutationOrigin->value
                     );
+                    foreach ($mutationCollection->mutations as $mutation) {
+                        $sourceValues = array_map(
+                            // like `ModelService::compileSources()`, but for a single item & without a nonce
+                            fn(SourceInterface $source) => $source instanceof SourceValueInterface
+                                ? $source->compile()
+                                : $this->modelService->serializeSource($source),
+                            $mutation->sources
+                        );
+                        $data[$scopeValue][$disposition->value][$mutationOriginValue][] = sprintf(
+                            '%s: %s %s',
+                            $mutation->mode->value,
+                            $mutation->directive->value,
+                            implode(' ', $sourceValues)
+                        );
+                    }
                 }
+                $data[$scopeValue][$disposition->value] = [
+                    '@policy' => $policy->compile($nonce),
+                    ...$data[$scopeValue][$disposition->value],
+                ];
             }
-            $data[$scopeValue] = ['@policy' => $policy->compile($nonce), ...$data[$scopeValue]];
         }
         ArrayUtility::naturalKeySortRecursive($data);
         return $data;