Skip to content

Commit 6e0922c

Browse files
dvesh3brusch
andauthored
[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>
1 parent 86f6caf commit 6e0922c

File tree

13 files changed

+391
-15
lines changed

13 files changed

+391
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Pimcore
7+
*
8+
* This source file is available under two different licenses:
9+
* - GNU General Public License version 3 (GPLv3)
10+
* - Pimcore Commercial License (PCL)
11+
* Full copyright and license information is available in
12+
* LICENSE.md which is distributed with this source code.
13+
*
14+
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
15+
* @license http://www.pimcore.org/license GPLv3 and PCL
16+
*/
17+
18+
namespace Pimcore\Bundle\AdminBundle\DependencyInjection\Compiler;
19+
20+
use Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler;
21+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
22+
use Symfony\Component\DependencyInjection\ContainerBuilder;
23+
24+
/**
25+
* @internal
26+
*/
27+
final class ContentSecurityPolicyUrlsPass implements CompilerPassInterface
28+
{
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function process(ContainerBuilder $container)
33+
{
34+
$definition = $container->getDefinition(ContentSecurityPolicyHandler::class);
35+
36+
37+
$config = $container->getParameter('pimcore_admin.config');
38+
39+
if (count($config['admin_csp_header']['additional_urls'])) {
40+
foreach ($config['admin_csp_header']['additional_urls'] as $additionalUrlsKey => $additionalUrlsArr) {
41+
$definition->addMethodCall('addAllowedUrls', [$additionalUrlsKey, $additionalUrlsArr]);
42+
}
43+
}
44+
}
45+
}

Diff for: bundles/AdminBundle/DependencyInjection/Configuration.php

+37
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
namespace Pimcore\Bundle\AdminBundle\DependencyInjection;
1717

18+
use Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler;
1819
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
1920
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
2021
use Symfony\Component\Config\Definition\ConfigurationInterface;
@@ -54,6 +55,42 @@ public function getConfigTreeBuilder(): TreeBuilder
5455
->end()
5556
->end()
5657
->end()
58+
->arrayNode('admin_csp_header')
59+
->canBeEnabled()
60+
->info('Can be used to enable or disable the Content Security Policy headers.')
61+
->children()
62+
->arrayNode('additional_urls')
63+
->addDefaultsIfNotSet()
64+
->normalizeKeys(false)
65+
->children()
66+
->arrayNode(ContentSecurityPolicyHandler::DEFAULT_OPT)
67+
->scalarPrototype()->end()
68+
->end()
69+
->arrayNode(ContentSecurityPolicyHandler::IMG_OPT)
70+
->scalarPrototype()->end()
71+
->end()
72+
->arrayNode(ContentSecurityPolicyHandler::SCRIPT_OPT)
73+
->scalarPrototype()->end()
74+
->end()
75+
->arrayNode(ContentSecurityPolicyHandler::STYLE_OPT)
76+
->scalarPrototype()->end()
77+
->end()
78+
->arrayNode(ContentSecurityPolicyHandler::CONNECT_OPT)
79+
->scalarPrototype()->end()
80+
->end()
81+
->arrayNode(ContentSecurityPolicyHandler::FONT_OPT)
82+
->scalarPrototype()->end()
83+
->end()
84+
->arrayNode(ContentSecurityPolicyHandler::MEDIA_OPT)
85+
->scalarPrototype()->end()
86+
->end()
87+
->arrayNode(ContentSecurityPolicyHandler::FRAME_OPT)
88+
->scalarPrototype()->end()
89+
->end()
90+
->end()
91+
->end()
92+
->end()
93+
->end()
5794
->scalarNode('custom_admin_path_identifier')
5895
->defaultNull()
5996
->validate()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
/**
4+
* Pimcore
5+
*
6+
* This source file is available under two different licenses:
7+
* - GNU General Public License version 3 (GPLv3)
8+
* - Pimcore Commercial License (PCL)
9+
* Full copyright and license information is available in
10+
* LICENSE.md which is distributed with this source code.
11+
*
12+
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
13+
* @license http://www.pimcore.org/license GPLv3 and PCL
14+
*/
15+
16+
namespace Pimcore\Bundle\AdminBundle\EventListener;
17+
18+
use Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler;
19+
use Pimcore\Bundle\CoreBundle\EventListener\Traits\PimcoreContextAwareTrait;
20+
use Pimcore\Config;
21+
use Pimcore\Http\Request\Resolver\PimcoreContextResolver;
22+
use Pimcore\Http\RequestHelper;
23+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
24+
use Symfony\Component\HttpKernel\Event\ResponseEvent;
25+
use Symfony\Component\HttpKernel\KernelEvents;
26+
27+
/**
28+
* @internal
29+
*/
30+
class AdminSecurityListener implements EventSubscriberInterface
31+
{
32+
use PimcoreContextAwareTrait;
33+
34+
/**
35+
* @param ContentSecurityPolicyHandler $contentSecurityPolicyHandler
36+
*/
37+
public function __construct(
38+
protected RequestHelper $requestHelper,
39+
protected ContentSecurityPolicyHandler $contentSecurityPolicyHandler,
40+
protected Config $config
41+
)
42+
{
43+
}
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
public static function getSubscribedEvents()
49+
{
50+
return [
51+
KernelEvents::RESPONSE => 'onKernelResponse',
52+
];
53+
}
54+
55+
public function onKernelResponse(ResponseEvent $event)
56+
{
57+
if (!$this->config['admin_csp_header']['enabled']) {
58+
return;
59+
}
60+
61+
$request = $event->getRequest();
62+
63+
if (!$event->isMainRequest()) {
64+
return;
65+
}
66+
67+
if (!$this->matchesPimcoreContext($request, PimcoreContextResolver::CONTEXT_ADMIN)) {
68+
return;
69+
}
70+
71+
if ($this->requestHelper->isFrontendRequestByAdmin($request)) {
72+
return;
73+
}
74+
75+
$response = $event->getResponse();
76+
77+
// set CSP header with random nonce string to the response
78+
$response->headers->set("Content-Security-Policy", $this->contentSecurityPolicyHandler->getCspHeader());
79+
}
80+
81+
}
82+
83+

Diff for: bundles/AdminBundle/PimcoreAdminBundle.php

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
namespace Pimcore\Bundle\AdminBundle;
1717

18+
use Pimcore\Bundle\AdminBundle\DependencyInjection\Compiler\ContentSecurityPolicyUrlsPass;
1819
use Pimcore\Bundle\AdminBundle\DependencyInjection\Compiler\GDPRDataProviderPass;
1920
use Pimcore\Bundle\AdminBundle\DependencyInjection\Compiler\ImportExportLocatorsPass;
2021
use Pimcore\Bundle\AdminBundle\DependencyInjection\Compiler\SerializerPass;
@@ -44,6 +45,7 @@ public function build(ContainerBuilder $container)
4445
$container->addCompilerPass(new GDPRDataProviderPass());
4546
$container->addCompilerPass(new ImportExportLocatorsPass());
4647
$container->addCompilerPass(new TranslationServicesPass());
48+
$container->addCompilerPass(new ContentSecurityPolicyUrlsPass());
4749

4850
/** @var SecurityExtension $extension */
4951
$extension = $container->getExtension('security');

Diff for: bundles/AdminBundle/Resources/config/event_listeners.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ services:
88
# SECURITY
99
#
1010

11+
Pimcore\Bundle\AdminBundle\EventListener\AdminSecurityListener: ~
1112
Pimcore\Bundle\AdminBundle\EventListener\BruteforceProtectionListener: ~
1213

1314
Pimcore\Bundle\AdminBundle\EventListener\AdminAuthenticationDoubleCheckListener:

Diff for: bundles/AdminBundle/Resources/config/security_services.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ services:
6565
tags:
6666
- { name: monolog.logger, channel: security }
6767

68+
Pimcore\Bundle\AdminBundle\Security\ContentSecurityPolicyHandler:
69+
public: true
70+
calls:
71+
- [ setLogger, [ '@logger' ] ]
72+
tags:
73+
- { name: monolog.logger, channel: security }
74+
6875
# user checker checking admin users for validity
6976
Pimcore\Bundle\AdminBundle\Security\User\UserChecker: ~
7077

Diff for: bundles/AdminBundle/Resources/public/js/pimcore/document/pages/preview.js

+9-4
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ pimcore.document.pages.preview = Class.create({
3131

3232
if (this.layout == null) {
3333

34-
var iframeOnLoad = "pimcore.globalmanager.get('document_" + this.page.id + "').preview.iFrameLoaded()";
35-
3634
// preview switcher only for pages not for emails
3735
var tbar = [];
3836
if(this.page.getType() == "page") {
@@ -124,9 +122,16 @@ pimcore.document.pages.preview = Class.create({
124122
scrollable: false,
125123
bodyStyle: "background:#323232;",
126124
bodyCls: "pimcore_overflow_scrolling",
127-
html: '<iframe src="about:blank" onload="' + iframeOnLoad + '" frameborder="0" ' +
125+
html: '<iframe src="about:blank" frameborder="0" ' +
128126
'style="width: 100%;background: #fff;" id="' + this.iframeName + '" ' +
129-
'name="' + this.iframeName + '"></iframe>'
127+
'name="' + this.iframeName + '"></iframe>',
128+
listeners: {
129+
afterrender: function () {
130+
Ext.get(this.getIframe()).on('load', function () {
131+
this.iFrameLoaded();
132+
}.bind(this));
133+
}.bind(this)
134+
}
130135
});
131136

132137
this.timeSlider = Ext.create('Ext.slider.Single', {

Diff for: bundles/AdminBundle/Resources/views/Admin/Index/index.html.twig

+10-10
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
8989
<title>{{ settings.hostname }} :: Pimcore</title>
9090
91-
<script>
91+
<script {{ pimcore_csp.getNonceHtmlAttribute()|raw }}>
9292
var pimcore = {}; // namespace
9393
9494
// hide symfony toolbar by default
@@ -98,8 +98,8 @@
9898
}
9999
</script>
100100
101-
<script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
102-
<script src="{{ path('fos_js_routing_js', {'callback' : 'fos.Router.setData'}) }}"></script>
101+
<script src="{{ asset('bundles/fosjsrouting/js/router.js') }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
102+
<script src="{{ path('fos_js_routing_js', {'callback' : 'fos.Router.setData'}) }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
103103
</head>
104104
105105
<body class="pimcore_version_10" data-app-env="{{ app.environment }}">
@@ -698,17 +698,17 @@
698698

699699
<!-- some javascript -->
700700
{# pimcore constants #}
701-
<script>
701+
<script {{ pimcore_csp.getNonceHtmlAttribute()|raw }}>
702702
pimcore.settings = {{(settings|json_encode(constant('JSON_PRETTY_PRINT'))|raw)}};
703703
</script>
704704

705-
<script src="{{ path('pimcore_admin_misc_jsontranslationssystem', {'language': language, '_dc': settings.build }) }}"></script>
706-
<script src="{{ path('pimcore_admin_user_getcurrentuser', {'_dc': settings.build }) }}"></script>
707-
<script src="{{ path('pimcore_admin_misc_availablelanguages', {'_dc': settings.build }) }}"></script>
705+
<script src="{{ path('pimcore_admin_misc_jsontranslationssystem', {'language': language, '_dc': settings.build }) }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
706+
<script src="{{ path('pimcore_admin_user_getcurrentuser', {'_dc': settings.build }) }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
707+
<script src="{{ path('pimcore_admin_misc_availablelanguages', {'_dc': settings.build }) }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
708708

709709
<!-- library scripts -->
710710
{% for scriptUrl in scriptLibs %}
711-
<script src="/bundles/pimcoreadmin/js/{{ scriptUrl }}?_dc={{ settings.build }}"></script>
711+
<script src="/bundles/pimcoreadmin/js/{{ scriptUrl }}?_dc={{ settings.build }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
712712
{% endfor %}
713713

714714
<!-- internal scripts -->
@@ -734,7 +734,7 @@
734734
<!-- bundle scripts -->
735735
{% if settings.disableMinifyJs %}
736736
{% for pluginJsPath in pluginJsPaths %}
737-
<script src="{{ pluginJsPath }}?_dc={{ pluginDcValue }}"></script>
737+
<script src="{{ pluginJsPath }}?_dc={{ pluginDcValue }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
738738
{% endfor %}
739739
{% else %}
740740
{{ pimcore_minimize_scripts(pluginJsPaths)|raw }}
@@ -745,6 +745,6 @@
745745
{% endfor %}
746746

747747
{# MUST BE THE LAST LINE #}
748-
<script src="/bundles/pimcoreadmin/js/pimcore/startup.js?_dc={{ settings.build }}"></script>
748+
<script src="/bundles/pimcoreadmin/js/pimcore/startup.js?_dc={{ settings.build }}" {{ pimcore_csp.getNonceHtmlAttribute()|raw }}></script>
749749
</body>
750750
</html>

0 commit comments

Comments
 (0)