Skip to content

Commit 8d9e199

Browse files
author
Oleksandr Gorkun
committed
MC-19926: Implement CSP
1 parent f2dd874 commit 8d9e199

File tree

16 files changed

+600
-18
lines changed

16 files changed

+600
-18
lines changed

app/code/Magento/Csp/Model/Collector/Config/FetchPolicyReader.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public function read(string $id, $value): PolicyInterface
3030
!empty($value['eval']),
3131
[],
3232
[],
33-
!empty($value['dynamic'])
33+
!empty($value['dynamic']),
34+
!empty($value['event_handlers'])
3435
);
3536
}
3637

app/code/Magento/Csp/Model/Collector/CspWhitelistXmlCollector.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ public function collect(array $defaultPolicies = []): array
4747
false,
4848
false,
4949
[],
50-
$values['hashes']
50+
$values['hashes'],
51+
false,
52+
false
5153
);
5254
}
5355

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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\Csp\Model\Collector;
9+
10+
use Magento\Csp\Api\Data\PolicyInterface;
11+
use Magento\Csp\Model\Policy\FetchPolicy;
12+
13+
/**
14+
* @inheritDoc
15+
*/
16+
class FetchPolicyMerger implements MergerInterface
17+
{
18+
/**
19+
* @inheritDoc
20+
*/
21+
public function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface
22+
{
23+
/** @var FetchPolicy $policy1 */
24+
/** @var FetchPolicy $policy2 */
25+
return new FetchPolicy(
26+
$policy1->getId(),
27+
$policy1->isNoneAllowed() || $policy2->isNoneAllowed(),
28+
array_unique(array_merge($policy1->getHostSources(), $policy2->getHostSources())),
29+
array_unique(array_merge($policy1->getSchemeSources(), $policy2->getSchemeSources())),
30+
$policy1->isSelfAllowed() || $policy2->isSelfAllowed(),
31+
$policy1->isInlineAllowed() || $policy2->isInlineAllowed(),
32+
$policy1->isEvalAllowed() || $policy2->isEvalAllowed(),
33+
array_unique(array_merge($policy1->getNonceValues(), $policy2->getNonceValues())),
34+
array_merge($policy1->getHashes(), $policy2->getHashes()),
35+
$policy1->isDynamicAllowed() || $policy2->isDynamicAllowed(),
36+
$policy1->areEventHandlersAllowed() || $policy2->areEventHandlersAllowed()
37+
);
38+
}
39+
40+
/**
41+
* @inheritDoc
42+
*/
43+
public function canMerge(PolicyInterface $policy1, PolicyInterface $policy2): bool
44+
{
45+
return ($policy1 instanceof FetchPolicy) && ($policy2 instanceof FetchPolicy);
46+
}
47+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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\Csp\Model\Collector;
9+
10+
use Magento\Csp\Api\Data\PolicyInterface;
11+
use Magento\Csp\Model\Policy\FlagPolicy;
12+
13+
/**
14+
* @inheritDoc
15+
*/
16+
class FlagPolicyMerger implements MergerInterface
17+
{
18+
/**
19+
* @inheritDoc
20+
*/
21+
public function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface
22+
{
23+
return $policy1;
24+
}
25+
26+
/**
27+
* @inheritDoc
28+
*/
29+
public function canMerge(PolicyInterface $policy1, PolicyInterface $policy2): bool
30+
{
31+
return ($policy1 instanceof FlagPolicy) && ($policy2 instanceof FlagPolicy);
32+
}
33+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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\Csp\Model\Collector;
9+
10+
use Magento\Csp\Api\Data\PolicyInterface;
11+
12+
/**
13+
* Merges policies with the same ID in order to have only 1 policy DTO-per-policy.
14+
*/
15+
interface MergerInterface
16+
{
17+
/**
18+
* Merges 2 found policies into 1.
19+
*
20+
* @param PolicyInterface $policy1
21+
* @param PolicyInterface $policy2
22+
* @return PolicyInterface
23+
*/
24+
public function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface;
25+
26+
/**
27+
* Whether current merger can merge given 2 policies.
28+
*
29+
* @param PolicyInterface $policy1
30+
* @param PolicyInterface $policy2
31+
* @return bool
32+
*/
33+
public function canMerge(PolicyInterface $policy1, PolicyInterface $policy2): bool;
34+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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\Csp\Model\Collector;
9+
10+
use Magento\Csp\Api\Data\PolicyInterface;
11+
use Magento\Csp\Model\Policy\PluginTypesPolicy;
12+
13+
/**
14+
* @inheritDoc
15+
*/
16+
class PluginTypesPolicyMerger implements MergerInterface
17+
{
18+
/**
19+
* @inheritDoc
20+
*/
21+
public function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface
22+
{
23+
/** @var PluginTypesPolicy $policy1 */
24+
/** @var PluginTypesPolicy $policy2 */
25+
return new PluginTypesPolicy(array_unique(array_merge($policy1->getTypes(), $policy2->getTypes())));
26+
}
27+
28+
/**
29+
* @inheritDoc
30+
*/
31+
public function canMerge(PolicyInterface $policy1, PolicyInterface $policy2): bool
32+
{
33+
return ($policy1 instanceof PluginTypesPolicy) && ($policy2 instanceof PluginTypesPolicy);
34+
}
35+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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\Csp\Model\Collector;
9+
10+
use Magento\Csp\Api\Data\PolicyInterface;
11+
use Magento\Csp\Model\Policy\SandboxPolicy;
12+
13+
/**
14+
* @inheritDoc
15+
*/
16+
class SandboxPolicyMerger implements MergerInterface
17+
{
18+
/**
19+
* @inheritDoc
20+
*/
21+
public function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface
22+
{
23+
/** @var SandboxPolicy $policy1 */
24+
/** @var SandboxPolicy $policy2 */
25+
return new SandboxPolicy(
26+
$policy1->isFormAllowed() || $policy2->isFormAllowed(),
27+
$policy1->isModalsAllowed() || $policy2->isModalsAllowed(),
28+
$policy1->isOrientationLockAllowed() || $policy2->isOrientationLockAllowed(),
29+
$policy1->isPointerLockAllowed() || $policy2->isPointerLockAllowed(),
30+
$policy1->isPopupsAllowed() || $policy2->isPopupsAllowed(),
31+
$policy1->isPopupsToEscapeSandboxAllowed() || $policy2->isPopupsToEscapeSandboxAllowed(),
32+
$policy1->isPresentationAllowed() || $policy2->isPresentationAllowed(),
33+
$policy1->isSameOriginAllowed() || $policy2->isSameOriginAllowed(),
34+
$policy1->isScriptsAllowed() || $policy2->isScriptsAllowed(),
35+
$policy1->isTopNavigationAllowed() || $policy2->isTopNavigationAllowed(),
36+
$policy1->isTopNavigationByUserActivationAllowed() || $policy2->isTopNavigationByUserActivationAllowed()
37+
);
38+
}
39+
40+
/**
41+
* @inheritDoc
42+
*/
43+
public function canMerge(PolicyInterface $policy1, PolicyInterface $policy2): bool
44+
{
45+
return ($policy1 instanceof SandboxPolicy) && ($policy2 instanceof SandboxPolicy);
46+
}
47+
}

app/code/Magento/Csp/Model/CompositePolicyCollector.php

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
namespace Magento\Csp\Model;
99

10+
use Magento\Csp\Api\Data\PolicyInterface;
1011
use Magento\Csp\Api\PolicyCollectorInterface;
12+
use Magento\Csp\Model\Collector\MergerInterface;
1113

1214
/**
1315
* Delegates collecting to multiple collectors.
@@ -19,12 +21,38 @@ class CompositePolicyCollector implements PolicyCollectorInterface
1921
*/
2022
private $collectors;
2123

24+
/**
25+
* @var MergerInterface[]
26+
*/
27+
private $mergers;
28+
2229
/**
2330
* @param PolicyCollectorInterface[] $collectors
31+
* @param MergerInterface[] $mergers
2432
*/
25-
public function __construct(array $collectors)
33+
public function __construct(array $collectors, array $mergers)
2634
{
2735
$this->collectors = $collectors;
36+
$this->mergers = $mergers;
37+
}
38+
39+
/**
40+
* Merge 2 policies with the same ID.
41+
*
42+
* @param PolicyInterface $policy1
43+
* @param PolicyInterface $policy2
44+
* @return PolicyInterface
45+
* @throws \RuntimeException When failed to merge.
46+
*/
47+
private function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface
48+
{
49+
foreach ($this->mergers as $merger) {
50+
if ($merger->canMerge($policy1, $policy2)) {
51+
return $merger->merge($policy1, $policy2);
52+
}
53+
}
54+
55+
throw new \RuntimeException(sprintf('Merge for policies #%s was not found', $policy1->getId()));
2856
}
2957

3058
/**
@@ -36,7 +64,17 @@ public function collect(array $defaultPolicies = []): array
3664
foreach ($this->collectors as $collector) {
3765
$collected = $collector->collect($collected);
3866
}
67+
//Merging policies.
68+
/** @var PolicyInterface[] $result */
69+
$result = [];
70+
foreach ($collected as $policy) {
71+
if (array_key_exists($policy->getId(), $result)) {
72+
$result[$policy->getId()] = $this->merge($result[$policy->getId()], $policy);
73+
} else {
74+
$result[$policy->getId()] = $policy;
75+
}
76+
}
3977

40-
return $collected;
78+
return array_values($result);
4179
}
4280
}

app/code/Magento/Csp/Model/Policy/FetchPolicy.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ class FetchPolicy implements SimplePolicyInterface
8282
*/
8383
private $dynamicAllowed;
8484

85+
/**
86+
* @var bool
87+
*/
88+
private $eventHandlersAllowed;
89+
8590
/**
8691
* @param string $id
8792
* @param bool $noneAllowed
@@ -93,6 +98,7 @@ class FetchPolicy implements SimplePolicyInterface
9398
* @param string[] $nonceValues
9499
* @param string[] $hashValues
95100
* @param bool $dynamicAllowed
101+
* @param bool $eventHandlersAllowed
96102
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
97103
*/
98104
public function __construct(
@@ -105,7 +111,8 @@ public function __construct(
105111
bool $evalAllowed = false,
106112
array $nonceValues = [],
107113
array $hashValues = [],
108-
bool $dynamicAllowed = false
114+
bool $dynamicAllowed = false,
115+
bool $eventHandlersAllowed = false
109116
) {
110117
$this->id = $id;
111118
$this->noneAllowed = $noneAllowed;
@@ -117,6 +124,7 @@ public function __construct(
117124
$this->nonceValues = array_unique($nonceValues);
118125
$this->hashes = $hashValues;
119126
$this->dynamicAllowed = $dynamicAllowed;
127+
$this->eventHandlersAllowed = $eventHandlersAllowed;
120128
}
121129

122130
/**
@@ -213,6 +221,9 @@ public function getValue(): string
213221
if ($this->isDynamicAllowed()) {
214222
$sources[] = '\'strict-dynamic\'';
215223
}
224+
if ($this->areEventHandlersAllowed()) {
225+
$sources[] = '\'unsafe-hashes\'';
226+
}
216227
foreach ($this->getNonceValues() as $nonce) {
217228
$sources[] = '\'nonce-' .base64_encode($nonce) .'\'';
218229
}
@@ -257,4 +268,14 @@ public function isDynamicAllowed(): bool
257268
{
258269
return $this->dynamicAllowed;
259270
}
271+
272+
/**
273+
* Allows to whitelist event handlers (but not javascript: URLs) with hashes.
274+
*
275+
* @return bool
276+
*/
277+
public function areEventHandlersAllowed(): bool
278+
{
279+
return $this->eventHandlersAllowed;
280+
}
260281
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
9+
<default>
10+
<csp>
11+
<mode>
12+
<storefront>
13+
<report_only>1</report_only>
14+
</storefront>
15+
<admin>
16+
<report_only>1</report_only>
17+
</admin>
18+
</mode>
19+
</csp>
20+
</default>
21+
</config>

0 commit comments

Comments
 (0)