Skip to content

Commit 2618eb7

Browse files
authored
Merge pull request #11 from fheinze/use-object-structure-for-csp-rules
FEATURE: config object format for directives to support merging CSP config split into different files
2 parents 30b6a62 + b4c6630 commit 2618eb7

13 files changed

Lines changed: 511 additions & 126 deletions

File tree

Classes/Command/CspConfigCommandController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class CspConfigCommandController extends CommandController
3131

3232
/**
3333
* @Flow\InjectConfiguration(path="content-security-policy")
34-
* @var string[][][]
34+
* @var array<string, array<string, array<string|int, string|bool>>>
3535
*/
3636
protected array $configuration;
3737

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flowpack\ContentSecurityPolicy\Exceptions;
6+
7+
use Neos\Flow\Exception;
8+
9+
class DirectivesNormalizerException extends Exception
10+
{
11+
public function __construct(string $reason)
12+
{
13+
parent::__construct(
14+
"Invalid yaml config provided. {$reason} Please check your settings.",
15+
);
16+
}
17+
}

Classes/Factory/PolicyFactory.php

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,74 @@
44

55
namespace Flowpack\ContentSecurityPolicy\Factory;
66

7+
use Flowpack\ContentSecurityPolicy\Exceptions\DirectivesNormalizerException;
78
use Flowpack\ContentSecurityPolicy\Exceptions\InvalidDirectiveException;
9+
use Flowpack\ContentSecurityPolicy\Helpers\DirectivesNormalizer;
810
use Flowpack\ContentSecurityPolicy\Model\Nonce;
911
use Flowpack\ContentSecurityPolicy\Model\Policy;
1012
use Neos\Flow\Annotations as Flow;
13+
use Psr\Log\LoggerInterface;
1114

1215
/**
1316
* @Flow\Scope("singleton")
1417
*/
1518
class PolicyFactory
1619
{
1720
/**
18-
* @param string[][] $defaultDirectives
19-
* @param string[][] $customDirectives
21+
* @Flow\InjectConfiguration(path="throw-invalid-directive-exception")
22+
*/
23+
protected bool $throwInvalidDirectiveException;
24+
25+
/**
26+
* @Flow\Inject
27+
*/
28+
protected LoggerInterface $logger;
29+
30+
/**
31+
* @Flow\Inject
32+
*
33+
*/
34+
35+
/**
36+
* @param array<string, array<string|int, string|bool>> $defaultDirectives
37+
* @param array<string, array<string|int, string|bool>> $customDirectives
2038
* @throws InvalidDirectiveException
39+
* @throws DirectivesNormalizerException
2140
*/
2241
public function create(Nonce $nonce, array $defaultDirectives, array $customDirectives): Policy
2342
{
24-
$resultDirectives = $defaultDirectives;
25-
foreach ($customDirectives as $key => $customDirective) {
43+
$normalizedDefaultDirectives = DirectivesNormalizer::normalize($defaultDirectives, $this->logger);
44+
$normalizedCustomDirectives = DirectivesNormalizer::normalize($customDirectives, $this->logger);
45+
46+
$resultDirectives = $normalizedDefaultDirectives;
47+
foreach ($normalizedCustomDirectives as $key => $customDirective) {
2648
if (array_key_exists($key, $resultDirectives)) {
2749
$resultDirectives[$key] = array_merge($resultDirectives[$key], $customDirective);
2850
} else {
2951
// Custom directive is not present in default, still needs to be added.
3052
$resultDirectives[$key] = $customDirective;
3153
}
32-
3354
$resultDirectives[$key] = array_unique($resultDirectives[$key]);
3455
}
3556

3657
$policy = new Policy();
3758
$policy->setNonce($nonce);
3859

3960
foreach ($resultDirectives as $directive => $values) {
40-
$policy->addDirective($directive, $values);
61+
try {
62+
$policy->addDirective($directive, $values);
63+
} catch (InvalidDirectiveException $e
64+
) {
65+
if ($this->throwInvalidDirectiveException) {
66+
// For development we want to make sure directives are configured correctly.
67+
throw $e;
68+
} else {
69+
// In production we just log the error and continue. If a directive is invalid, we still
70+
// want to apply the rest of the policy.
71+
$this->logger->critical($e->getMessage());
72+
continue;
73+
}
74+
}
4175
}
4276

4377
return $policy;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flowpack\ContentSecurityPolicy\Helpers;
6+
7+
use Flowpack\ContentSecurityPolicy\Exceptions\DirectivesNormalizerException;
8+
use Psr\Log\LoggerInterface;
9+
10+
/**
11+
* Helper to support normalization of directives from different formats.
12+
* The old format supported yaml lists. Now key-value pairs should be used for directives.
13+
* In the future we will deprecate the list format!
14+
*
15+
* We also cleanup of empty directives and entries here before further processing.
16+
*/
17+
class DirectivesNormalizer
18+
{
19+
/**
20+
* @param array<string, array<string|int, string|bool>> $directives
21+
* @return string[][]
22+
* @throws DirectivesNormalizerException
23+
*/
24+
public static function normalize(array $directives, LoggerInterface $logger): array
25+
{
26+
$result = [];
27+
// directives e.g. script-src:
28+
foreach ($directives as $directive => $values) {
29+
if(is_array($values) && sizeof($values) > 0) {
30+
$normalizedValues = [];
31+
$firstKeyType = null;
32+
// values e.g. 'self', 'unsafe-inline' OR key-value pairs e.g. example.com: true
33+
foreach ($values as $key => $value) {
34+
$keyType = gettype($key);
35+
$valueType = gettype($value);
36+
if ($firstKeyType === null) {
37+
$firstKeyType = $keyType;
38+
} else {
39+
if ($keyType !== $firstKeyType) {
40+
// we do not allow mixed key types -> this should be marked as an error in the IDE as well
41+
// as Flow should throw an exception here. But just to be sure, we add this check.
42+
throw new DirectivesNormalizerException('Directives must be defined as a list OR an object.');
43+
}
44+
}
45+
46+
if($keyType === 'integer' && $valueType === 'string' && !empty($value)) {
47+
// old configuration format using list
48+
$normalizedValues[] = $value;
49+
$logger->warning('Using list format for CSP directives is deprecated and will be removed in future versions. Please use key-value pairs with boolean values instead.');
50+
} elseif($keyType === 'string') {
51+
// new configuration format using key-value pairs
52+
if($valueType === 'boolean') {
53+
if($value === true && !empty($key)) {
54+
$normalizedValues[] = $key;
55+
}
56+
} else {
57+
// We chose a format similar to NodeType constraints yaml configuration.
58+
throw new DirectivesNormalizerException('When using keys in your yaml, the values must be boolean.');
59+
}
60+
}
61+
}
62+
if(!empty($normalizedValues)) {
63+
// we also clean up empty directives here
64+
$result[$directive] = $normalizedValues;
65+
}
66+
}
67+
}
68+
69+
return $result;
70+
}
71+
}

Classes/Http/CspHeaderMiddleware.php

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Flowpack\ContentSecurityPolicy\Http;
66

77
use Exception;
8+
use Flowpack\ContentSecurityPolicy\Exceptions\DirectivesNormalizerException;
89
use Flowpack\ContentSecurityPolicy\Exceptions\InvalidDirectiveException;
910
use Flowpack\ContentSecurityPolicy\Factory\PolicyFactory;
1011
use Flowpack\ContentSecurityPolicy\Helpers\TagHelper;
@@ -16,7 +17,6 @@
1617
use Psr\Http\Message\ServerRequestInterface;
1718
use Psr\Http\Server\MiddlewareInterface;
1819
use Psr\Http\Server\RequestHandlerInterface;
19-
use Psr\Log\LoggerInterface;
2020

2121
class CspHeaderMiddleware implements MiddlewareInterface
2222
{
@@ -27,11 +27,6 @@ class CspHeaderMiddleware implements MiddlewareInterface
2727
*/
2828
protected bool $enabled;
2929

30-
/**
31-
* @Flow\Inject
32-
*/
33-
protected LoggerInterface $logger;
34-
3530
/**
3631
* @Flow\Inject
3732
*/
@@ -44,12 +39,14 @@ class CspHeaderMiddleware implements MiddlewareInterface
4439

4540
/**
4641
* @Flow\InjectConfiguration(path="content-security-policy")
47-
* @var string[][][]
42+
* @var array<string, array<string, array<string|int, string|bool>>>
4843
*/
4944
protected array $configuration;
5045

5146
/**
5247
* @inheritDoc
48+
* @throws InvalidDirectiveException
49+
* @throws DirectivesNormalizerException
5350
*/
5451
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
5552
{
@@ -58,13 +55,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
5855
return $response;
5956
}
6057

61-
try {
62-
$policy = $this->getPolicyByCurrentContext($request);
63-
} catch (Exception $exception) {
64-
$this->logger->critical($exception->getMessage(), ['exception' => $exception]);
65-
66-
return $response;
67-
}
58+
$policy = $this->getPolicyByCurrentContext($request);
6859

6960
if ($policy->hasNonceDirectiveValue()) {
7061
$body = $response->getBody();
@@ -78,6 +69,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
7869

7970
/**
8071
* @throws InvalidDirectiveException
72+
* @throws DirectivesNormalizerException
8173
*/
8274
private function getPolicyByCurrentContext(ServerRequestInterface $request): Policy
8375
{
@@ -131,7 +123,7 @@ private function checkTagAndReplaceUsingACallback(
131123
function ($hits) use ($hitCallback) {
132124
$tagMarkup = $hits[0];
133125
$tagName = $hits[1];
134-
126+
135127
return call_user_func(
136128
$hitCallback,
137129
$tagMarkup,

Classes/Model/Policy.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Flowpack\ContentSecurityPolicy\Exceptions\InvalidDirectiveException;
88
use Neos\Flow\Annotations as Flow;
9+
use Psr\Log\LoggerInterface;
910

1011
class Policy
1112
{
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Flowpack:
2+
ContentSecurityPolicy:
3+
throw-invalid-directive-exception: false

Configuration/Settings.yaml

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,69 +2,70 @@ Flowpack:
22
ContentSecurityPolicy:
33
enabled: true
44
report-only: false
5+
throw-invalid-directive-exception: true
56
content-security-policy:
67
default:
78
base-uri:
8-
- 'self'
9+
'self': true
910
connect-src:
10-
- 'self'
11+
'self': true
1112
default-src:
12-
- 'self'
13+
'self': true
1314
form-action:
14-
- 'self'
15+
'self': true
1516
img-src:
16-
- 'self'
17+
'self': true
1718
media-src:
18-
- 'self'
19+
'self': true
1920
frame-src:
20-
- 'self'
21+
'self': true
2122
object-src:
22-
- 'self'
23+
'self': true
2324
script-src:
24-
- 'self'
25+
'self': true
2526
style-src:
26-
- 'self'
27+
'self': true
2728
style-src-attr:
28-
- 'self'
29+
'self': true
2930
style-src-elem:
30-
- 'self'
31+
'self': true
3132
font-src:
32-
- 'self'
33+
'self': true
3334
custom: [ ]
3435
backend:
3536
base-uri:
36-
- 'self'
37+
'self': true
3738
connect-src:
38-
- 'self'
39+
'self': true
3940
default-src:
40-
- 'self'
41+
'self': true
4142
form-action:
42-
- 'self'
43+
'self': true
4344
img-src:
44-
- 'self'
45-
- 'data:'
45+
'self': true
46+
'data:': true
4647
media-src:
47-
- 'self'
48+
'self': true
4849
frame-src:
49-
- 'self'
50+
'self': true
5051
object-src:
51-
- 'self'
52+
'self': true
5253
script-src:
53-
- 'self'
54-
- 'unsafe-inline'
55-
- 'unsafe-eval'
54+
'self': true
55+
'unsafe-inline': true
56+
'unsafe-eval': true
5657
style-src:
57-
- 'self'
58-
- 'unsafe-inline'
58+
'self': true
59+
'unsafe-inline': true
5960
style-src-attr:
60-
- 'self'
61-
- 'unsafe-inline'
61+
'self': true
62+
'unsafe-inline': true
6263
style-src-elem:
63-
- 'self'
64-
- 'unsafe-inline'
64+
'self': true
65+
'unsafe-inline': true
6566
font-src:
66-
- 'self'
67-
- 'data:'
67+
'self': true
68+
'data:': true
6869
custom-backend: [ ]
6970

7071
Neos:

0 commit comments

Comments
 (0)