Skip to content

Commit 478ea9b

Browse files
authored
Merge pull request #8826 from magento-cia/AC-10685-final
AC-10685: [PCI] CSP enforced on payment pages
2 parents 22056b4 + 613191f commit 478ea9b

File tree

31 files changed

+681
-57
lines changed

31 files changed

+681
-57
lines changed

app/code/Magento/AdminAnalytics/ViewModel/Metadata.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
namespace Magento\AdminAnalytics\ViewModel;
1010

1111
use Magento\Config\Model\Config\Backend\Admin\Custom;
12+
use Magento\Csp\Helper\CspNonceProvider;
1213
use Magento\Framework\App\Config\ScopeConfigInterface;
14+
use Magento\Framework\App\ObjectManager;
1315
use Magento\Framework\App\ProductMetadataInterface;
1416
use Magento\Backend\Model\Auth\Session;
1517
use Magento\Framework\App\State;
@@ -21,6 +23,11 @@
2123
*/
2224
class Metadata implements ArgumentInterface
2325
{
26+
/**
27+
* @var string
28+
*/
29+
private $nonce;
30+
2431
/**
2532
* @var State
2633
*/
@@ -41,22 +48,33 @@ class Metadata implements ArgumentInterface
4148
*/
4249
private $config;
4350

51+
/**
52+
* @var CspNonceProvider
53+
*/
54+
private $nonceProvider;
55+
4456
/**
4557
* @param ProductMetadataInterface $productMetadata
4658
* @param Session $authSession
4759
* @param State $appState
4860
* @param ScopeConfigInterface $config
61+
* @param CspNonceProvider|null $nonceProvider
4962
*/
5063
public function __construct(
5164
ProductMetadataInterface $productMetadata,
5265
Session $authSession,
5366
State $appState,
54-
ScopeConfigInterface $config
67+
ScopeConfigInterface $config,
68+
CspNonceProvider $nonceProvider = null
5569
) {
5670
$this->productMetadata = $productMetadata;
5771
$this->authSession = $authSession;
5872
$this->appState = $appState;
5973
$this->config = $config;
74+
75+
$this->nonceProvider = $nonceProvider ?: ObjectManager::getInstance()->get(CspNonceProvider::class);
76+
77+
$this->nonce = $this->nonceProvider->generateNonce();
6078
}
6179

6280
/**
@@ -156,4 +174,14 @@ public function getCurrentUserRoleName(): string
156174
{
157175
return $this->authSession->getUser()->getRole()->getRoleName();
158176
}
177+
178+
/**
179+
* Get a random nonce for each request.
180+
*
181+
* @return string
182+
*/
183+
public function getNonce(): string
184+
{
185+
return $this->nonce;
186+
}
159187
}

app/code/Magento/AdminAnalytics/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"magento/module-config": "*",
1212
"magento/module-store": "*",
1313
"magento/module-ui": "*",
14-
"magento/module-release-notification": "*"
14+
"magento/module-release-notification": "*",
15+
"magento/module-csp": "*"
1516
},
1617
"type": "magento2-module",
1718
"license": [

app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
/**
88
* @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer
9+
* @var \Magento\Framework\Escaper $escaper
910
*/
1011
?>
1112

@@ -22,18 +23,25 @@
2223
<?php
2324
/** @var \Magento\AdminAnalytics\ViewModel\Metadata $metadata */
2425
$metadata = $block->getMetadata();
26+
$nonce = $escaper->escapeJs($metadata->getNonce());
2527
$scriptString = '
2628
var adminAnalyticsMetadata = {
27-
"secure_base_url": "' . $block->escapeJs($metadata->getSecureBaseUrlForScope()) . '",
28-
"version": "' . $block->escapeJs($metadata->getMagentoVersion()) . '",
29-
"product_edition": "' . $block->escapeJs($metadata->getProductEdition()) . '",
30-
"user": "' . $block->escapeJs($metadata->getCurrentUser()) . '",
31-
"mode": "' . $block->escapeJs($metadata->getMode()) . '",
32-
"store_name_default": "' . $block->escapeJs($metadata->getStoreNameForScope()) . '",
33-
"admin_user_created": "' . $block->escapeJs($metadata->getCurrentUserCreatedDate()) . '",
34-
"admin_user_logdate": "' . $block->escapeJs($metadata->getCurrentUserLogDate()) . '",
35-
"admin_user_role_name": "' . $block->escapeJs($metadata->getCurrentUserRoleName()) . '"
29+
"secure_base_url": "' . $escaper->escapeJs($metadata->getSecureBaseUrlForScope()) . '",
30+
"version": "' . $escaper->escapeJs($metadata->getMagentoVersion()) . '",
31+
"product_edition": "' . $escaper->escapeJs($metadata->getProductEdition()) . '",
32+
"user": "' . $escaper->escapeJs($metadata->getCurrentUser()) . '",
33+
"mode": "' . $escaper->escapeJs($metadata->getMode()) . '",
34+
"store_name_default": "' . $escaper->escapeJs($metadata->getStoreNameForScope()) . '",
35+
"admin_user_created": "' . $escaper->escapeJs($metadata->getCurrentUserCreatedDate()) . '",
36+
"admin_user_logdate": "' . $escaper->escapeJs($metadata->getCurrentUserLogDate()) . '",
37+
"admin_user_role_name": "' . $escaper->escapeJs($metadata->getCurrentUserRoleName()) . '"
3638
};
39+
40+
var digitalData = {
41+
"nonce": "' . $nonce . '"
42+
};
43+
44+
var cspNonce = "' . $nonce . '";
3745
';
3846
?>
3947
<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?>

app/code/Magento/Backend/etc/adminhtml/system.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
<tab id="general" translate="label" sortOrder="100">
1111
<label>General</label>
1212
</tab>
13+
<tab id="security" translate="label" sortOrder="200">
14+
<label>Security</label>
15+
</tab>
1316
<tab id="service" translate="label" sortOrder="99999">
1417
<label>Services</label>
1518
</tab>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Checkout\Observer;
9+
10+
use Magento\Csp\Model\Collector\DynamicCollector;
11+
use Magento\Csp\Model\Policy\FetchPolicy;
12+
use Magento\Framework\Event\Observer;
13+
use Magento\Framework\Event\ObserverInterface;
14+
use Magento\Framework\Translate\InlineInterface;
15+
16+
/**
17+
* Observer for adding CSP policy for inline translation
18+
*/
19+
class CspPolicyObserver implements ObserverInterface
20+
{
21+
/**
22+
* @var InlineInterface
23+
*/
24+
private InlineInterface $inlineTranslate;
25+
26+
/**
27+
* @var DynamicCollector
28+
*/
29+
private DynamicCollector $dynamicCollector;
30+
31+
/**
32+
* @param InlineInterface $inlineTranslate
33+
* @param DynamicCollector $dynamicCollector
34+
*/
35+
public function __construct(InlineInterface $inlineTranslate, DynamicCollector $dynamicCollector)
36+
{
37+
$this->inlineTranslate = $inlineTranslate;
38+
$this->dynamicCollector = $dynamicCollector;
39+
}
40+
41+
/**
42+
* Override CSP policy for checkout page wit inline translation
43+
*
44+
* @param Observer $observer
45+
* @return void
46+
*
47+
* @throws \Exception
48+
*
49+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
50+
*/
51+
public function execute(Observer $observer): void
52+
{
53+
if ($this->inlineTranslate->isAllowed()) {
54+
$policy = new FetchPolicy(
55+
'script-src',
56+
false,
57+
[],
58+
[],
59+
true,
60+
true,
61+
false,
62+
[],
63+
[]
64+
);
65+
66+
$this->dynamicCollector->add($policy);
67+
}
68+
}
69+
}

app/code/Magento/Checkout/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"magento/module-tax": "*",
2727
"magento/module-theme": "*",
2828
"magento/module-ui": "*",
29-
"magento/module-authorization": "*"
29+
"magento/module-authorization": "*",
30+
"magento/module-csp": "*"
3031
},
3132
"suggest": {
3233
"magento/module-cookie": "*"

app/code/Magento/Checkout/etc/adminhtml/system.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,17 @@
106106
</field>
107107
</group>
108108
</section>
109+
<section id="csp">
110+
<group id="mode">
111+
<group id="storefront_checkout_index_index" translate="label" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1">
112+
<label>Storefront > One Page Checkout</label>
113+
<field id="report_uri" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
114+
<label>Report URI</label>
115+
<comment>If empty, Default Report URI for storefront will be used.</comment>
116+
<validate>validate-url</validate>
117+
</field>
118+
</group>
119+
</group>
120+
</section>
109121
</system>
110122
</config>

app/code/Magento/Checkout/etc/config.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,20 @@
5656
</shown_to_logged_in_user>
5757
</captcha>
5858
</customer>
59+
<csp>
60+
<mode>
61+
<storefront_checkout_index_index>
62+
<report_only>0</report_only>
63+
</storefront_checkout_index_index>
64+
</mode>
65+
<policies>
66+
<storefront_checkout_index_index>
67+
<scripts>
68+
<inline>0</inline>
69+
<event_handlers>1</event_handlers>
70+
</scripts>
71+
</storefront_checkout_index_index>
72+
</policies>
73+
</csp>
5974
</default>
6075
</config>

app/code/Magento/Checkout/etc/frontend/events.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@
1212
<event name="customer_logout">
1313
<observer name="unsetAll" instance="Magento\Checkout\Observer\UnsetAllObserver" />
1414
</event>
15+
<event name="controller_action_predispatch_checkout_index_index">
16+
<observer name="cps_storefront_checkout_index_index_predispatch"
17+
instance="Magento\Checkout\Observer\CspPolicyObserver"/>
18+
</event>
1519
</config>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Csp\Helper;
10+
11+
use Magento\Csp\Model\Collector\DynamicCollector;
12+
use Magento\Csp\Model\Policy\FetchPolicy;
13+
use Magento\Framework\Exception\LocalizedException;
14+
use Magento\Framework\Math\Random;
15+
16+
/**
17+
* This helper class is used to provide nonce for CSP
18+
*
19+
* It also adds a nonce to the CSP header.
20+
*/
21+
class CspNonceProvider
22+
{
23+
/**
24+
* @var string
25+
*/
26+
private const NONCE_LENGTH = 32;
27+
28+
/**
29+
* @var string
30+
*/
31+
private string $nonce;
32+
33+
/**
34+
* @var Random
35+
*/
36+
private Random $random;
37+
38+
/**
39+
* @var DynamicCollector
40+
*/
41+
private DynamicCollector $dynamicCollector;
42+
43+
/**
44+
* @param Random $random
45+
* @param DynamicCollector $dynamicCollector
46+
*/
47+
public function __construct(
48+
Random $random,
49+
DynamicCollector $dynamicCollector
50+
) {
51+
$this->random = $random;
52+
$this->dynamicCollector = $dynamicCollector;
53+
}
54+
55+
/**
56+
* Generate nonce and add it to the CSP header
57+
*
58+
* @return string
59+
* @throws LocalizedException
60+
*/
61+
public function generateNonce(): string
62+
{
63+
if (empty($this->nonce)) {
64+
$this->nonce = $this->random->getRandomString(
65+
self::NONCE_LENGTH,
66+
Random::CHARS_DIGITS . Random::CHARS_LOWERS
67+
);
68+
69+
$policy = new FetchPolicy(
70+
'script-src',
71+
false,
72+
[],
73+
[],
74+
false,
75+
false,
76+
false,
77+
[$this->nonce],
78+
[]
79+
);
80+
81+
$this->dynamicCollector->add($policy);
82+
}
83+
84+
return base64_encode($this->nonce);
85+
}
86+
}

0 commit comments

Comments
 (0)