CVE-2022-0911: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - Stored in GitHub repository pimcore/pimcore prior to 10.4.0.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2022-03-16T09:15Z
Repository: https://github.com/pimcore/pimcore
Language: PHP
Files changed: bundles/AdminBundle/DependencyInjection/Compiler/ContentSecurityPolicyUrlsPass.php, bundles/AdminBundle/DependencyInjection/Configuration.php, bundles/AdminBundle/EventListener/AdminSecurityListener.php, bundles/AdminBundle/PimcoreAdminBundle.php, bundles/AdminBundle/Resources/config/event_listeners.yaml, bundles/AdminBundle/Resources/config/security_services.yaml, bundles/AdminBundle/Resources/public/js/pimcore/document/pages/preview.js, bundles/AdminBundle/Resources/views/Admin/Index/index.html.twig, bundles/AdminBundle/Security/ContentSecurityPolicyHandler.php, bundles/CoreBundle/Resources/config/pimcore/default.yaml, bundles/EcommerceFrameworkBundle/Resources/views/back-office.html.twig, doc/Development_Documentation/26_Best_Practice/75_Security_Concept.md, lib/Twig/Extension/Templating/HeadScript.php
Commit date: 2022-03-14 14:19:09 +0100
Commit message: [Admin] Security - Add handler to enable Content Security Policy (#11447)

* [Admin] Security - Add handler to enable Content Security Policy

* [Admin] Security - Add handler to enable Content Security Policy

* [Admin] Security - Add handler to enable Content Security Policy - fix document preview

* [Admin] Security - Add handler to enable Content Security Policy - add docs

* [Admin] Security - Add handler to enable Content Security Policy - review changes

* [Admin] Security - Add handler to enable Content Security Policy - review changes

* [Admin] Security - Add handler to enable Content Security Policy - review changes

* [Admin] Security - Add handler to enable Content Security Policy - review changes

* added missing config node + micro optimizations

Co-authored-by: Bernhard Rusch <bernhard.rusch@elements.at>
Diff: diff --git a/bundles/AdminBundle/DependencyInjection/Compiler/ContentSecurityPolicyUrlsPass.php b/bundles/AdminBundle/DependencyInjection/Compiler/ContentSecurityPolicyUrlsPass.php
new file mode 100644
index 000000000000..c8812eadf45e
--- /dev/null
+++ b/bundles/AdminBundle/DependencyInjection/Compiler/ContentSecurityPolicyUrlsPass.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * Pimcore
+ *
+ * This source file is available under two different licenses:
+ * - GNU General Public License version 3 (GPLv3)
+ * - Pimcore Commercial License (PCL)
+ * Full copyright and license information is available in
+ * LICENSE.md which is distributed with this source code.
+ *
+ * @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
+ * @license    http://www.pimcore.org/license     GPLv3 and PCL
+ */
+
+namespace Pimcore\Bundle\AdminBundle\DependencyInjection\Compiler;
+
+use Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+/**
+ * @internal
+ */
+final class ContentSecurityPolicyUrlsPass implements CompilerPassInterface
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function process(ContainerBuilder $container)
+    {
+        $definition = $container->getDefinition(ContentSecurityPolicyHandler::class);
+
+
+        $config = $container->getParameter('pimcore_admin.config');
+
+        if (count($config['admin_csp_header']['additional_urls'])) {
+            foreach ($config['admin_csp_header']['additional_urls'] as $additionalUrlsKey => $additionalUrlsArr) {
+                $definition->addMethodCall('addAllowedUrls', [$additionalUrlsKey, $additionalUrlsArr]);
+            }
+        }
+    }
+}
diff --git a/bundles/AdminBundle/DependencyInjection/Configuration.php b/bundles/AdminBundle/DependencyInjection/Configuration.php
index 2addf96d78da..121c6c6eb220 100644
--- a/bundles/AdminBundle/DependencyInjection/Configuration.php
+++ b/bundles/AdminBundle/DependencyInjection/Configuration.php
@@ -13,10 +13,11 @@
  *  @license    http://www.pimcore.org/license     GPLv3 and PCL
  */
 
 namespace Pimcore\Bundle\AdminBundle\DependencyInjection;
 
+use Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler;
 use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
 use Symfony\Component\Config\Definition\Builder\TreeBuilder;
 use Symfony\Component\Config\Definition\ConfigurationInterface;
 
 /**
@@ -52,10 +53,46 @@ final class Configuration implements ConfigurationInterface
                         ->prototype('scalar')
                         ->end()
                     ->end()
                 ->end()
             ->end()
+            ->arrayNode('admin_csp_header')
+                ->canBeEnabled()
+                ->info('Can be used to enable or disable the Content Security Policy headers.')
+                ->children()
+                    ->arrayNode('additional_urls')
+                        ->addDefaultsIfNotSet()
+                        ->normalizeKeys(false)
+                        ->children()
+                            ->arrayNode(ContentSecurityPolicyHandler::DEFAULT_OPT)
+                                ->scalarPrototype()->end()
+                            ->end()
+                            ->arrayNode(ContentSecurityPolicyHandler::IMG_OPT)
+                                ->scalarPrototype()->end()
+                            ->end()
+                            ->arrayNode(ContentSecurityPolicyHandler::SCRIPT_OPT)
+                                ->scalarPrototype()->end()
+                            ->end()
+                            ->arrayNode(ContentSecurityPolicyHandler::STYLE_OPT)
+                                ->scalarPrototype()->end()
+                            ->end()
+                            ->arrayNode(ContentSecurityPolicyHandler::CONNECT_OPT)
+                                ->scalarPrototype()->end()
+                            ->end()
+                            ->arrayNode(ContentSecurityPolicyHandler::FONT_OPT)
+                                ->scalarPrototype()->end()
+                            ->end()
+                            ->arrayNode(ContentSecurityPolicyHandler::MEDIA_OPT)
+                                ->scalarPrototype()->end()
+                            ->end()
+                            ->arrayNode(ContentSecurityPolicyHandler::FRAME_OPT)
+                                ->scalarPrototype()->end()
+                            ->end()
+                        ->end()
+                    ->end()
+                ->end()
+            ->end()
             ->scalarNode('custom_admin_path_identifier')
                 ->defaultNull()
                 ->validate()
                     ->ifTrue(function ($v) {
                         return strlen($v) < 20;
diff --git a/bundles/AdminBundle/EventListener/AdminSecurityListener.php b/bundles/AdminBundle/EventListener/AdminSecurityListener.php
new file mode 100644
index 000000000000..2c9b62972408
--- /dev/null
+++ b/bundles/AdminBundle/EventListener/AdminSecurityListener.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * Pimcore
+ *
+ * This source file is available under two different licenses:
+ * - GNU General Public License version 3 (GPLv3)
+ * - Pimcore Commercial License (PCL)
+ * Full copyright and license information is available in
+ * LICENSE.md which is distributed with this source code.
+ *
+ * @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
+ * @license    http://www.pimcore.org/license     GPLv3 and PCL
+ */
+
+namespace Pimcore\Bundle\AdminBundle\EventListener;
+
+use Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler;
+use Pimcore\Bundle\CoreBundle\EventListener\Traits\PimcoreContextAwareTrait;
+use Pimcore\Config;
+use Pimcore\Http\Request\Resolver\PimcoreContextResolver;
+use Pimcore\Http\RequestHelper;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\ResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * @internal
+ */
+class AdminSecurityListener implements EventSubscriberInterface
+{
+    use PimcoreContextAwareTrait;
+
+    /**
+     * @param ContentSecurityPolicyHandler $contentSecurityPolicyHandler
+     */
+    public function __construct(
+        protected RequestHelper $requestHelper,
+        protected ContentSecurityPolicyHandler $contentSecurityPolicyHandler,
+        protected Config $config
+    )
+    {
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public static function getSubscribedEvents()
+    {
+        return [
+            KernelEvents::RESPONSE => 'onKernelResponse',
+        ];
+    }
+
+    public function onKernelResponse(ResponseEvent $event)
+    {
+        if (!$this->config['admin_csp_header']['enabled']) {
+            return;
+        }
+
+        $request = $event->getRequest();
+
+        if (!$event->isMainRequest()) {
+            return;
+        }
+
+        if (!$this->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN)) {
+            return;
+        }
+
+        if ($this->requestHelper->isFrontendRequestByAdmin($request)) {
+            return;
+        }
+
+        $response = $event->getResponse();
+
+        // set CSP header with random nonce string to the response
+        $response->headers->set("Content-Security-Policy", $this->contentSecurityPolicyHandler->getCspHeader());
+    }
+
+}
+
+
diff --git a/bundles/AdminBundle/PimcoreAdminBundle.php b/bundles/AdminBundle/PimcoreAdminBundle.php
index 14be87f92638..45e7230f455d 100644
--- a/bundles/AdminBundle/PimcoreAdminBundle.php
+++ b/bundles/AdminBundle/PimcoreAdminBundle.php
@@ -13,10 +13,11 @@
  *  @license    http://www.pimcore.org/license     GPLv3 and PCL
  */
 
 namespace Pimcore\Bundle\AdminBundle;
 
+use Pimcore\Bundle\AdminBundle\DependencyInjection\Compiler\ContentSecurityPolicyUrlsPass;
 use Pimcore\Bundle\AdminBundle\DependencyInjection\Compiler\GDPRDataProviderPass;
 use Pimcore\Bundle\AdminBundle\DependencyInjection\Compiler\ImportExportLocatorsPass;
 use Pimcore\Bundle\AdminBundle\DependencyInjection\Compiler\SerializerPass;
 use Pimcore\Bundle\AdminBundle\DependencyInjection\Compiler\TranslationServicesPass;
 use Pimcore\Bundle\AdminBundle\GDPR\DataProvider\DataProviderInterface;
@@ -42,10 +43,11 @@ class PimcoreAdminBundle extends Bundle
 
         $container->addCompilerPass(new SerializerPass());
         $container->addCompilerPass(new GDPRDataProviderPass());
         $container->addCompilerPass(new ImportExportLocatorsPass());
         $container->addCompilerPass(new TranslationServicesPass());
+        $container->addCompilerPass(new ContentSecurityPolicyUrlsPass());
 
         /** @var SecurityExtension $extension */
         $extension = $container->getExtension('security');
         $extension->addSecurityListenerFactory(new PreAuthenticatedAdminSessionFactory());
     }
diff --git a/bundles/AdminBundle/Resources/config/event_listeners.yaml b/bundles/AdminBundle/Resources/config/event_listeners.yaml
index 5127ac41a0e7..21694a62f9ac 100644
--- a/bundles/AdminBundle/Resources/config/event_listeners.yaml
+++ b/bundles/AdminBundle/Resources/config/event_listeners.yaml
@@ -6,10 +6,11 @@ services:
 
     #
     # SECURITY
     #
 
+    Pimcore\Bundle\AdminBundle\EventListener\AdminSecurityListener: ~
     Pimcore\Bundle\AdminBundle\EventListener\BruteforceProtectionListener: ~
 
     Pimcore\Bundle\AdminBundle\EventListener\AdminAuthenticationDoubleCheckListener:
         arguments:
             $unauthenticatedRoutes: '%pimcore.admin.unauthenticated_routes%'
diff --git a/bundles/AdminBundle/Resources/config/security_services.yaml b/bundles/AdminBundle/Resources/config/security_services.yaml
index b9018e5cdebb..3047722e17dd 100644
--- a/bundles/AdminBundle/Resources/config/security_services.yaml
+++ b/bundles/AdminBundle/Resources/config/security_services.yaml
@@ -63,8 +63,15 @@ services:
         calls:
             - [setLogger, ['@logger']]
         tags:
             - { name: monolog.logger, channel: security }
 
+    Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler:
+        public: true
+        calls:
+            - [ setLogger, [ '@logger' ] ]
+        tags:
+            - { name: monolog.logger, channel: security }
+
     # user checker checking admin users for validity
     Pimcore\Bundle\AdminBundle\Security\User\UserChecker: ~
 
diff --git a/bundles/AdminBundle/Resources/public/js/pimcore/document/pages/preview.js b/bundles/AdminBundle/Resources/public/js/pimcore/document/pages/preview.js
index 2bdfbeea5b26..9734ced0c047 100644
--- a/bundles/AdminBundle/Resources/public/js/pimcore/document/pages/preview.js
+++ b/bundles/AdminBundle/Resources/public/js/pimcore/document/pages/preview.js
@@ -29,12 +29,10 @@ pimcore.document.pages.preview = Class.create({
 
     getLayout: function () {
 
         if (this.layout == null) {
 
-            var iframeOnLoad = "pimcore.globalmanager.get('document_" + this.page.id + "').preview.iFrameLoaded()";
-
             // preview switcher only for pages not for emails
             var tbar = [];
             if(this.page.getType() == "page") {
 
                 tbar = [{
@@ -122,13 +120,20 @@ pimcore.document.pages.preview = Class.create({
                 border: false,
                 region: "center",
                 scrollable: false,
                 bodyStyle: "background:#323232;",
                 bodyCls: "pimcore_overflow_scrolling",
-                html: '<iframe src="about:blank" onload="' + iframeOnLoad + '" frameborder="0" ' +
+                html: '<iframe src="about:blank" frameborder="0" ' +
                     'style="width: 100%;background: #fff;" id="' + this.iframeName + '" ' +
-                    'name="' + this.iframeName + '"></iframe>'
+                    'name="' + this.iframeName + '"></iframe>',
+                listeners: {
+                    afterrender: function () {
+                        Ext.get(this.getIframe()).on('load', function () {
+                            this.iFrameLoaded();
+                        }.bind(this));
+                    }.bind(this)
+                }
             });
 
             this.timeSlider = Ext.create('Ext.slider.Single', {
                 region: 'center',
                 width: '100%',
diff --git a/bundles/AdminBundle/Resources/views/Admin/Index/index.html.twig b/bundles/AdminBundle/Resources/views/Admin/Index/index.html.twig
index 7b51eefd1ffb..589466d617ac 100644
--- a/bundles/AdminBundle/Resources/views/Admin/Index/index.html.twig
+++ b/bundles/AdminBundle/Resources/views/Admin/Index/index.html.twig
@@ -86,22 +86,22 @@
         }
     </style>
 
     <title>{{ settings.hostname }} :: Pimcore</title>
 
-    <script>
+    <script {{ pimcore_csp.getNonceHtmlAttribute()|raw }}>
         var pimcore = {}; // namespace
 
         // hide symfony toolbar by default
         var symfonyToolbarKey = 'symfony/profiler/toolbar/displayState';
         if(!window.localStorage.getItem(symfonyToolbarKey)) {
             window.localStorage.setItem(symfonyToolbarKey, 'none');
         }
     </script>
 
-    <script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
-    <script src="{{ path('fos_js_routing_js', {'callback' : 'fos.Router.setData'}) }}"></script>
+    <script src="{{ asset('bundles/fosjsrouting/js/router.js') }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
+    <script src="{{ path('fos_js_routing_js', {'callback' : 'fos.Router.setData'}) }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
 </head>
 
 <body class="pimcore_version_10" data-app-env="{{ app.environment }}">
 
 <div id="pimcore_loading">
@@ -696,21 +696,21 @@
 ]
 %}
 
 <!-- some javascript -->
 {# pimcore constants #}
-<script>
+<script {{ pimcore_csp.getNonceHtmlAttribute()|raw }}>
     pimcore.settings = {{(settings|json_encode(constant('JSON_PRETTY_PRINT'))|raw)}};
 </script>
 
-<script src="{{ path('pimcore_admin_misc_jsontranslationssystem', {'language': language, '_dc': settings.build }) }}"></script>
-<script src="{{ path('pimcore_admin_user_getcurrentuser', {'_dc': settings.build }) }}"></script>
-<script src="{{ path('pimcore_admin_misc_availablelanguages', {'_dc': settings.build }) }}"></script>
+<script src="{{ path('pimcore_admin_misc_jsontranslationssystem', {'language': language, '_dc': settings.build }) }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
+<script src="{{ path('pimcore_admin_user_getcurrentuser', {'_dc': settings.build }) }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
+<script src="{{ path('pimcore_admin_misc_availablelanguages', {'_dc': settings.build }) }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
 
 <!-- library scripts -->
 {% for scriptUrl in scriptLibs %}
-    <script src="/bundles/pimcoreadmin/js/{{ scriptUrl }}?_dc={{ settings.build }}"></script>
+    <script src="/bundles/pimcoreadmin/js/{{ scriptUrl }}?_dc={{ settings.build }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
 {% endfor %}
 
 <!-- internal scripts -->
 {% if settings.disableMinifyJs %}
     {% for scriptUrl in scripts %}
@@ -732,19 +732,19 @@
 {% endif %}
 
 <!-- bundle scripts -->
 {% if settings.disableMinifyJs %}
     {% for pluginJsPath in pluginJsPaths %}
-        <script src="{{ pluginJsPath }}?_dc={{ pluginDcValue }}"></script>
+        <script src="{{ pluginJsPath }}?_dc={{ pluginDcValue }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
     {% endfor %}
 {% else %}
     {{ pimcore_minimize_scripts(pluginJsPaths)|raw }}
 {% endif %}
 
 {% for pluginCssPath in pluginCssPaths %}
     <link rel="stylesheet" type="text/css" href="{{ pluginCssPath }}?_dc={{ pluginDcValue }}"/>
 {% endfor %}
 
 {#  MUST BE THE LAST LINE  #}
-<script src="/bundles/pimcoreadmin/js/pimcore/startup.js?_dc={{ settings.build }}"></script>
+<script src="/bundles/pimcoreadmin/js/pimcore/startup.js?_dc={{ settings.build }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
 </body>
 </html>
diff --git a/bundles/AdminBundle/Security/ContentSecurityPolicyHandler.php b/bundles/AdminBundle/Security/ContentSecurityPolicyHandler.php
new file mode 100644
index 000000000000..bc6b14511059
--- /dev/null
+++ b/bundles/AdminBundle/Security/ContentSecurityPolicyHandler.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * Pimcore
+ *
+ * This source file is available under two different licenses:
+ * - GNU General Public License version 3 (GPLv3)
+ * - Pimcore Commercial License (PCL)
+ * Full copyright and license information is available in
+ * LICENSE.md which is distributed with this source code.
+ *
+ * @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
+ * @license    http://www.pimcore.org/license     GPLv3 and PCL
+ */
+
+namespace Pimcore\Bundle\AdminBundle\Security;
+
+use Pimcore\Config;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+/**
+ * @internal
+ */
+class ContentSecurityPolicyHandler implements LoggerAwareInterface
+{
+    use LoggerAwareTrait;
+
+    /** @var String|null */
+    private ?string $nonce = null;
+
+    public const DEFAULT_OPT = 'default-src';
+    public const IMG_OPT = 'img-src';
+    public const SCRIPT_OPT = 'script-src';
+    public const STYLE_OPT = 'style-src';
+    public const CONNECT_OPT = 'connect-src';
+    public const FONT_OPT = 'font-src';
+    public const MEDIA_OPT = 'media-src';
+    public const FRAME_OPT = 'frame-src';
+
+    /**
+     * @var array
+     */
+    private array $allowedUrls = [
+        self::CONNECT_OPT => [
+            'https://liveupdate.pimcore.org/update-check', //AdminBundle statistics & update-check service
+            'https://nominatim.openstreetmap.org/' //CoreBundle geocoding_url_template
+        ],
+    ];
+
+    public function __construct(protected Config $config, protected array $cspHeaderOptions = [])
+    {
+        $resolver = new OptionsResolver();
+        $this->configureOptions($resolver);
+
+        $this->cspHeaderOptions = $resolver->resolve($cspHeaderOptions);
+    }
+
+    public function configureOptions(OptionsResolver $resolver)
+    {
+        $resolver->setDefaults([
+            self::DEFAULT_OPT => "'self'",
+            self::IMG_OPT => "* data: blob:",
+            self::MEDIA_OPT => "'self' data:",
+            self::SCRIPT_OPT => "'self' 'nonce-" . $this->getNonce() . "' 'unsafe-inline' 'unsafe-eval'",
+            self::STYLE_OPT => "'self' 'unsafe-inline'",
+            self::FRAME_OPT => "'self'",
+            self::CONNECT_OPT => "'self' blob:",
+            self::FONT_OPT => "'self'",
+        ]);
+    }
+
+    /**
+     * @return string
+     */
+    public function getCspHeader(): string
+    {
+        $cspHeaderOptions = array_map(function ($k, $v) {
+            return "$k $v " . $this->getAllowedUrls($k);
+        }, array_keys($this->cspHeaderOptions), array_values($this->cspHeaderOptions));
+
+        return implode(';' ,$cspHeaderOptions);
+    }
+
+    /**
+     * @param string $key
+     * @param bool $flatten
+     *
+     * @return array|string
+     */
+    private function getAllowedUrls(string $key, bool $flatten = true): array|string
+    {
+        if (!$flatten) {
+            return $this->allowedUrls[$key] ?? [];
+        }
+
+        return isset($this->allowedUrls[$key]) && is_array($this->allowedUrls[$key]) ? implode(' ', $this->allowedUrls[$key]) : '';
+    }
+
+    /**
+     * @param string $key
+     * @param array $value
+     *
+     * @return $this
+     */
+    public function addAllowedUrls(string $key, array $value): self
+    {
+        if(!isset($this->allowedUrls[$key])) {
+            $this->allowedUrls[$key] = [];
+        }
+
+        foreach ($value as $val) {
+            $this->allowedUrls[$key][] = $val;
+        }
+
+        return $this;
+    }
+
+    /**
+     * @param string $key
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function setCspHeader(string $key, string $value): self
+    {
+        $this->cspHeaderOptions[$key] = $value;
+
+        return $this;
+    }
+
+    /**
+     *
+     * @return string
+     */
+    public function getNonceHtmlAttribute(): string
+    {
+        return $this->config['admin_csp_header']['enabled'] ? ' nonce="' . $this->getNonce() . '"' : '';
+    }
+
+    /**
+     * Generates a random nonce parameter.
+     *
+     * @return string
+     */
+    private function getNonce(): string
+    {
+        if (!$this->nonce) {
+            $this->nonce = generateRandomSymfonySecret();
+        }
+
+        return $this->nonce;
+    }
+}
diff --git a/bundles/CoreBundle/Resources/config/pimcore/default.yaml b/bundles/CoreBundle/Resources/config/pimcore/default.yaml
index f8a7a0f42d49..2b4cc7ce6784 100644
--- a/bundles/CoreBundle/Resources/config/pimcore/default.yaml
+++ b/bundles/CoreBundle/Resources/config/pimcore/default.yaml
@@ -57,10 +57,11 @@ twig:
     globals:
         # avoid fetching and using services from templates if possible
         # this is only here for compatibility/dev reasons and may be removed later
         container: '@service_container'
         pimcore_csrf: '@Pimcore\Bundle\AdminBundle\Security\CsrfProtectionHandler'
+        pimcore_csp: '@Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler'
     paths:
         '%kernel.project_dir%/templates': App
 
 # Doctrine Configuration
 doctrine:
diff --git a/bundles/EcommerceFrameworkBundle/Resources/views/back-office.html.twig b/bundles/EcommerceFrameworkBundle/Resources/views/back-office.html.twig
index dbdcf9f77dca..0599b614989b 100644
--- a/bundles/EcommerceFrameworkBundle/Resources/views/back-office.html.twig
+++ b/bundles/EcommerceFrameworkBundle/Resources/views/back-office.html.twig
@@ -74,11 +74,11 @@
     {# output scripts added before #}
     {% block scripts deferred %}
         {{ pimcore_head_script() }}
     {% endblock %}
 
-    <script>
+    <script {{ pimcore_csp.getNonceHtmlAttribute()|raw }}>
         var pimcore = parent.pimcore;
         var popups = document.getElementsByClassName('popup-modal');
         for (var i = 0; i < popups.length; i++) {
             var element = popups[i];
 
diff --git a/doc/Development_Documentation/26_Best_Practice/75_Security_Concept.md b/doc/Development_Documentation/26_Best_Practice/75_Security_Concept.md
index b17813a1ca4f..854a06e063dc 100644
--- a/doc/Development_Documentation/26_Best_Practice/75_Security_Concept.md
+++ b/doc/Development_Documentation/26_Best_Practice/75_Security_Concept.md
@@ -23,10 +23,46 @@ dependencies. All Dependencies are managed through Composer (the standard PHP de
 it easy and comfortable to keep all dependencies of Pimcore and your project up-to-date and safe. 
 
 Since Pimcore is a Symfony application, it can utilize all Symfony tools, like the 
 [Symfony Security Checker](https://symfony.com/doc/5.2/security/security_checker.html). 
 
+### Content Security Policy
+Pimcore provides a Content Security Policy handler, which enables an additional security layer to protect from certain attacks like Cross-Site Scripting (XSS) and data injection and so on, by adding `Content-Security-Policy` HTTP response header with [nonce](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce) to every request in Admin interface. The generated nonce encoded string is matched with the one provided in link or inline javascript, which allows them to be executed safely. 
+
+Read more about [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP).
+
+To enable CSP, add a custom configuration in `config/config.yaml`:
+```yaml
+# config/config.yaml
+pimcore_admin:
+    admin_csp_header:
+        enabled: true
+```
+
+And to allow external urls for each directive, you can provide a list in the configuration:
+```yaml
+# config/config.yaml
+pimcore_admin:
+    admin_csp_header:
+        additional_urls:
+            script_src:
+                - 'https://oreo.cat/scripts/meow.js' 
+                - 'https://bagheera.cat/*'
+            style-src:
+                - 'https://oreo.cat/scripts/meow.css'
+```
+
+In case, you are using third party bundles or custom implementation that extends the admin backend interface with custom views then you would need to use generated nonce string in your scripts.
+If a script does not contain valid a nonce, then it is stopped from being executed wih a warning in console like:
+
+`Refused to execute inline script because it violates the following Content Security Policy directive: ...`
+
+This issue can be resolved either by using Pimcore [Headscript extension](../02_MVC/02_Template/02_Template_Extensions/03_HeadScript.md) or add nonce script to inline scripts as follows:
+
+```twig
+<script {{ pimcore_csp.getNonceHtmlAttribute()|raw }}>
+```
 
 ### Handling Security Issues
 In the case of a security issue/vulnerability in the Pimcore core framework, we handle them with the following procedure: 
 - **Reporting Issue**: 
 Report issue via [Pimcore Security form](https://pimcorehq.wufoo.com/forms/pimcore-security-report/), not via public 
diff --git a/lib/Twig/Extension/Templating/HeadScript.php b/lib/Twig/Extension/Templating/HeadScript.php
index ad4abeac3573..3b39f5322fb4 100644
--- a/lib/Twig/Extension/Templating/HeadScript.php
+++ b/lib/Twig/Extension/Templating/HeadScript.php
@@ -36,10 +36,11 @@
  * @license    http://framework.zend.com/license/new-bsd     New BSD License
  */
 
 namespace Pimcore\Twig\Extension\Templating;
 
+use Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler;
 use Pimcore\Event\FrontendEvents;
 use Pimcore\Twig\Extension\Templating\Placeholder\CacheBusterAware;
 use Pimcore\Twig\Extension\Templating\Placeholder\Container;
 use Pimcore\Twig\Extension\Templating\Placeholder\ContainerService;
 use Pimcore\Twig\Extension\Templating\Placeholder\Exception;
@@ -480,10 +481,13 @@ class HeadScript extends CacheBusterAware implements RuntimeExtensionInterface
                 }
                 $attrString .= sprintf(' %s="%s"', $key, ($this->_autoEscape) ? $this->_escape($value) : $value);
             }
         }
 
+        /** @var ContentSecurityPolicyHandler $cspHandler */
+        $cspHandler = \Pimcore::getContainer()->get(ContentSecurityPolicyHandler::class);
+        $attrString .= $cspHandler->getNonceHtmlAttribute();
         $addScriptEscape = !(isset($item->attributes['noescape']) && filter_var($item->attributes['noescape'], FILTER_VALIDATE_BOOLEAN));
 
         $html = '<script' . $attrString . '>';
         if (!empty($item->source)) {
             $html .= PHP_EOL ;

Vulnerable code:             var iframeOnLoad = "pimcore.globalmanager.get('document_" + this.page.id + "').preview.iFrameLoaded()";

                html: '<iframe src="about:blank" onload="' + iframeOnLoad + '" frameborder="0" ' +
                    'name="' + this.iframeName + '"></iframe>'
    <script>
    <script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
    <script src="{{ path('fos_js_routing_js', {'callback' : 'fos.Router.setData'}) }}"></script>
<script>
<script src="{{ path('pimcore_admin_misc_jsontranslationssystem', {'language': language, '_dc': settings.build }) }}"></script>
<script src="{{ path('pimcore_admin_user_getcurrentuser', {'_dc': settings.build }) }}"></script>
<script src="{{ path('pimcore_admin_misc_availablelanguages', {'_dc': settings.build }) }}"></script>
    <script src="/bundles/pimcoreadmin/js/{{ scriptUrl }}?_dc={{ settings.build }}"></script>
        <script src="{{ pluginJsPath }}?_dc={{ pluginDcValue }}"></script>
<script src="/bundles/pimcoreadmin/js/pimcore/startup.js?_dc={{ settings.build }}"></script>
    <script>
Fixed code: <?php

declare(strict_types=1);

/**
 * Pimcore
 *
 * This source file is available under two different licenses:
 * - GNU General Public License version 3 (GPLv3)
 * - Pimcore Commercial License (PCL)
 * Full copyright and license information is available in
 * LICENSE.md which is distributed with this source code.
 *
 * @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
 * @license    http://www.pimcore.org/license     GPLv3 and PCL
 */

namespace Pimcore\Bundle\AdminBundle\DependencyInjection\Compiler;

use Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * @internal
 */
final class ContentSecurityPolicyUrlsPass implements CompilerPassInterface
{
    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        $definition = $container->getDefinition(ContentSecurityPolicyHandler::class);


        $config = $container->getParameter('pimcore_admin.config');

        if (count($config['admin_csp_header']['additional_urls'])) {
            foreach ($config['admin_csp_header']['additional_urls'] as $additionalUrlsKey => $additionalUrlsArr) {
                $definition->addMethodCall('addAllowedUrls', [$additionalUrlsKey, $additionalUrlsArr]);
            }
        }
    }
}
use Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler;
            ->arrayNode('admin_csp_header')
                ->canBeEnabled()
                ->info('Can be used to enable or disable the Content Security Policy headers.')
                ->children()
                    ->arrayNode('additional_urls')
                        ->addDefaultsIfNotSet()
                        ->normalizeKeys(false)
                        ->children()
                            ->arrayNode(ContentSecurityPolicyHandler::DEFAULT_OPT)
                                ->scalarPrototype()->end()
                            ->end()
                            ->arrayNode(ContentSecurityPolicyHandler::IMG_OPT)
                                ->scalarPrototype()->end()
                            ->end()
                            ->arrayNode(ContentSecurityPolicyHandler::SCRIPT_OPT)
                                ->scalarPrototype()->end()
                            ->end()
                            ->arrayNode(ContentSecurityPolicyHandler::STYLE_OPT)
                                ->scalarPrototype()->end()
                            ->end()
                            ->arrayNode(ContentSecurityPolicyHandler::CONNECT_OPT)
                                ->scalarPrototype()->end()
                            ->end()
                            ->arrayNode(ContentSecurityPolicyHandler::FONT_OPT)
                                ->scalarPrototype()->end()
                            ->end()
                            ->arrayNode(ContentSecurityPolicyHandler::MEDIA_OPT)
                                ->scalarPrototype()->end()
                            ->end()
                            ->arrayNode(ContentSecurityPolicyHandler::FRAME_OPT)
                                ->scalarPrototype()->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
            ->end()
<?php

/**
 * Pimcore
 *
 * This source file is available under two different licenses:
 * - GNU General Public License version 3 (GPLv3)
 * - Pimcore Commercial License (PCL)
 * Full copyright and license information is available in
 * LICENSE.md which is distributed with this source code.
 *
 * @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
 * @license    http://www.pimcore.org/license     GPLv3 and PCL
 */

namespace Pimcore\Bundle\AdminBundle\EventListener;

use Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler;
use Pimcore\Bundle\CoreBundle\EventListener\Traits\PimcoreContextAwareTrait;
use Pimcore\Config;
use Pimcore\Http\Request\Resolver\PimcoreContextResolver;
use Pimcore\Http\RequestHelper;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
 * @internal
 */
class AdminSecurityListener implements EventSubscriberInterface
{
    use PimcoreContextAwareTrait;

    /**
     * @param ContentSecurityPolicyHandler $contentSecurityPolicyHandler
     */
    public function __construct(
        protected RequestHelper $requestHelper,
        protected ContentSecurityPolicyHandler $contentSecurityPolicyHandler,
        protected Config $config
    )
    {
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::RESPONSE => 'onKernelResponse',
        ];
    }

    public function onKernelResponse(ResponseEvent $event)
    {
        if (!$this->config['admin_csp_header']['enabled']) {
            return;
        }

        $request = $event->getRequest();

        if (!$event->isMainRequest()) {
            return;
        }

        if (!$this->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN)) {
            return;
        }

        if ($this->requestHelper->isFrontendRequestByAdmin($request)) {
            return;
        }

        $response = $event->getResponse();

        // set CSP header with random nonce string to the response
        $response->headers->set("Content-Security-Policy", $this->contentSecurityPolicyHandler->getCspHeader());
    }

}


use Pimcore\Bundle\AdminBundle\DependencyInjection\Compiler\ContentSecurityPolicyUrlsPass;
        $container->addCompilerPass(new ContentSecurityPolicyUrlsPass());
    Pimcore\Bundle\AdminBundle\EventListener\AdminSecurityListener: ~
    Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler:
        public: true
        calls:
            - [ setLogger, [ '@logger' ] ]
        tags:
            - { name: monolog.logger, channel: security }

                html: '<iframe src="about:blank" frameborder="0" ' +
                    'name="' + this.iframeName + '"></iframe>',
                listeners: {
                    afterrender: function () {
                        Ext.get(this.getIframe()).on('load', function () {
                            this.iFrameLoaded();
                        }.bind(this));
                    }.bind(this)
                }
    <script {{ pimcore_csp.getNonceHtmlAttribute()|raw }}>
    <script src="{{ asset('bundles/fosjsrouting/js/router.js') }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
    <script src="{{ path('fos_js_routing_js', {'callback' : 'fos.Router.setData'}) }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
<script {{ pimcore_csp.getNonceHtmlAttribute()|raw }}>
<script src="{{ path('pimcore_admin_misc_jsontranslationssystem', {'language': language, '_dc': settings.build }) }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
<script src="{{ path('pimcore_admin_user_getcurrentuser', {'_dc': settings.build }) }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
<script src="{{ path('pimcore_admin_misc_availablelanguages', {'_dc': settings.build }) }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
    <script src="/bundles/pimcoreadmin/js/{{ scriptUrl }}?_dc={{ settings.build }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
        <script src="{{ pluginJsPath }}?_dc={{ pluginDcValue }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
<script src="/bundles/pimcoreadmin/js/pimcore/startup.js?_dc={{ settings.build }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
<?php

/**
 * Pimcore
 *
 * This source file is available under two different licenses:
 * - GNU General Public License version 3 (GPLv3)
 * - Pimcore Commercial License (PCL)
 * Full copyright and license information is available in
 * LICENSE.md which is distributed with this source code.
 *
 * @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
 * @license    http://www.pimcore.org/license     GPLv3 and PCL
 */

namespace Pimcore\Bundle\AdminBundle\Security;

use Pimcore\Config;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * @internal
 */
class ContentSecurityPolicyHandler implements LoggerAwareInterface
{
    use LoggerAwareTrait;

    /** @var String|null */
    private ?string $nonce = null;

    public const DEFAULT_OPT = 'default-src';
    public const IMG_OPT = 'img-src';
    public const SCRIPT_OPT = 'script-src';
    public const STYLE_OPT = 'style-src';
    public const CONNECT_OPT = 'connect-src';
    public const FONT_OPT = 'font-src';
    public const MEDIA_OPT = 'media-src';
    public const FRAME_OPT = 'frame-src';

    /**
     * @var array
     */
    private array $allowedUrls = [
        self::CONNECT_OPT => [
            'https://liveupdate.pimcore.org/update-check', //AdminBundle statistics & update-check service
            'https://nominatim.openstreetmap.org/' //CoreBundle geocoding_url_template
        ],
    ];

    public function __construct(protected Config $config, protected array $cspHeaderOptions = [])
    {
        $resolver = new OptionsResolver();
        $this->configureOptions($resolver);

        $this->cspHeaderOptions = $resolver->resolve($cspHeaderOptions);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            self::DEFAULT_OPT => "'self'",
            self::IMG_OPT => "* data: blob:",
            self::MEDIA_OPT => "'self' data:",
            self::SCRIPT_OPT => "'self' 'nonce-" . $this->getNonce() . "' 'unsafe-inline' 'unsafe-eval'",
            self::STYLE_OPT => "'self' 'unsafe-inline'",
            self::FRAME_OPT => "'self'",
            self::CONNECT_OPT => "'self' blob:",
            self::FONT_OPT => "'self'",
        ]);
    }

    /**
     * @return string
     */
    public function getCspHeader(): string
    {
        $cspHeaderOptions = array_map(function ($k, $v) {
            return "$k $v " . $this->getAllowedUrls($k);
        }, array_keys($this->cspHeaderOptions), array_values($this->cspHeaderOptions));

        return implode(';' ,$cspHeaderOptions);
    }

    /**
     * @param string $key
     * @param bool $flatten
     *
     * @return array|string
     */
    private function getAllowedUrls(string $key, bool $flatten = true): array|string
    {
        if (!$flatten) {
            return $this->allowedUrls[$key] ?? [];
        }

        return isset($this->allowedUrls[$key]) && is_array($this->allowedUrls[$key]) ? implode(' ', $this->allowedUrls[$key]) : '';
    }

    /**
     * @param string $key
     * @param array $value
     *
     * @return $this
     */
    public function addAllowedUrls(string $key, array $value): self
    {
        if(!isset($this->allowedUrls[$key])) {
            $this->allowedUrls[$key] = [];
        }

        foreach ($value as $val) {
            $this->allowedUrls[$key][] = $val;
        }

        return $this;
    }

    /**
     * @param string $key
     * @param string $value
     *
     * @return $this
     */
    public function setCspHeader(string $key, string $value): self
    {
        $this->cspHeaderOptions[$key] = $value;

        return $this;
    }

    /**
     *
     * @return string
     */
    public function getNonceHtmlAttribute(): string
    {
        return $this->config['admin_csp_header']['enabled'] ? ' nonce="' . $this->getNonce() . '"' : '';
    }

    /**
     * Generates a random nonce parameter.
     *
     * @return string
     */
    private function getNonce(): string
    {
        if (!$this->nonce) {
            $this->nonce = generateRandomSymfonySecret();
        }

        return $this->nonce;
    }
}
        pimcore_csp: '@Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler'
    <script {{ pimcore_csp.getNonceHtmlAttribute()|raw }}>
### Content Security Policy
Pimcore provides a Content Security Policy handler, which enables an additional security layer to protect from certain attacks like Cross-Site Scripting (XSS) and data injection and so on, by adding `Content-Security-Policy` HTTP response header with [nonce](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce) to every request in Admin interface. The generated nonce encoded string is matched with the one provided in link or inline javascript, which allows them to be executed safely. 

Read more about [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP).

To enable CSP, add a custom configuration in `config/config.yaml`:
```yaml
# config/config.yaml
pimcore_admin:
    admin_csp_header:
        enabled: true
```

And to allow external urls for each directive, you can provide a list in the configuration:
```yaml
# config/config.yaml
pimcore_admin:
    admin_csp_header:
        additional_urls:
            script_src:
                - 'https://oreo.cat/scripts/meow.js' 
                - 'https://bagheera.cat/*'
            style-src:
                - 'https://oreo.cat/scripts/meow.css'
```

In case, you are using third party bundles or custom implementation that extends the admin backend interface with custom views then you would need to use generated nonce string in your scripts.
If a script does not contain valid a nonce, then it is stopped from being executed wih a warning in console like:

`Refused to execute inline script because it violates the following Content Security Policy directive: ...`

This issue can be resolved either by using Pimcore [Headscript extension](../02_MVC/02_Template/02_Template_Extensions/03_HeadScript.md) or add nonce script to inline scripts as follows:

```twig
<script {{ pimcore_csp.getNonceHtmlAttribute()|raw }}>
```
use Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler;
        /** @var ContentSecurityPolicyHandler $cspHandler */
        $cspHandler = \Pimcore::getContainer()->get(ContentSecurityPolicyHandler::class);
        $attrString .= $cspHandler->getNonceHtmlAttribute();