Skip to content

Commit 8124ac5

Browse files
committed
Add Cross-Origin Policy feature with configurable headers (COEP, COOP, CORP)
1 parent b1c5e32 commit 8124ac5

File tree

9 files changed

+170
-4
lines changed

9 files changed

+170
-4
lines changed

src/ContentSecurityPolicy/PolicyManager.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ public function getAvailableDirective(Request $request): array
5151
return $this->getFirefoxDirectives();
5252
case UserAgentParserInterface::BROWSER_SAFARI:
5353
return $this->getLevel1();
54+
default:
55+
return [];
5456
}
55-
56-
return [];
5757
}
5858

5959
/**

src/DependencyInjection/Configuration.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ public function getConfigTreeBuilder(): TreeBuilder
6666
->append($this->addCspNode())
6767

6868
->append($this->addReferrerPolicyNode())
69+
70+
->append($this->addCrossOriginPolicyNodes())
6971
->end()
7072
->end();
7173

@@ -392,4 +394,30 @@ private function getFlexibleSslNode(): ArrayNodeDefinition
392394

393395
return $node;
394396
}
397+
398+
private function addCrossOriginPolicyNodes(): ArrayNodeDefinition
399+
{
400+
$node = new ArrayNodeDefinition('cross_origin_policy');
401+
$node
402+
->canBeEnabled()
403+
->children()
404+
->enumNode('coep')
405+
->defaultValue('unsafe-none')
406+
->values(['unsafe-none', 'require-corp', 'credentialless'])
407+
->info('Cross-Origin-Embedder-Policy (COEP) header value')
408+
->end()
409+
->enumNode('coop')
410+
->defaultValue('unsafe-none')
411+
->values(['unsafe-none', 'same-origin-allow-popups', 'same-origin', 'noopener-allow-popups'])
412+
->info('Cross-Origin-Opener-Policy (COOP) header value')
413+
->end()
414+
->enumNode('corp')
415+
->defaultValue('same-site')
416+
->values(['same-site', 'same-origin', 'cross-origin'])
417+
->info('Cross-Origin Resource Policy (CORP) header value')
418+
->end()
419+
->end();
420+
421+
return $node;
422+
}
395423
}

src/DependencyInjection/NelmioSecurityExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ public function load(array $configs, ContainerBuilder $container): void
176176
$loader->load('referrer_policy.php');
177177
$container->setParameter('nelmio_security.referrer_policy.policies', $config['referrer_policy']['policies']);
178178
}
179+
180+
if ($this->isConfigEnabled($container, $config['cross_origin_policy'])) {
181+
$loader->load('cross_origin_policy.php');
182+
$container->setParameter('nelmio_security.cross_origin_policy.coep', $config['cross_origin_policy']['coep']);
183+
$container->setParameter('nelmio_security.cross_origin_policy.coop', $config['cross_origin_policy']['coop']);
184+
$container->setParameter('nelmio_security.cross_origin_policy.corp', $config['cross_origin_policy']['corp']);
185+
}
179186
}
180187

181188
/**
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Nelmio SecurityBundle.
7+
*
8+
* (c) Nelmio <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Nelmio\SecurityBundle\EventListener;
15+
16+
use Symfony\Component\HttpKernel\Event\ResponseEvent;
17+
18+
/**
19+
* @author Florent Morselli <[email protected]>
20+
*/
21+
final class CrossOriginPolicyListener
22+
{
23+
private string $coep;
24+
private string $coop;
25+
private string $corp;
26+
27+
public function __construct(string $coep, string $coop, string $corp)
28+
{
29+
$this->coep = $coep;
30+
$this->coop = $coop;
31+
$this->corp = $corp;
32+
}
33+
34+
public function onKernelResponse(ResponseEvent $e): void
35+
{
36+
if (!$e->isMainRequest()) {
37+
return;
38+
}
39+
40+
$response = $e->getResponse();
41+
42+
$response->headers->set('Cross-Origin-Embedder-Policy', $this->coep);
43+
$response->headers->set('Cross-Origin-Opener-Policy', $this->coop);
44+
$response->headers->set('Cross-Origin-Resource-Policy', $this->corp);
45+
}
46+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Nelmio SecurityBundle.
7+
*
8+
* (c) Nelmio <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
use Nelmio\SecurityBundle\EventListener\CrossOriginPolicyListener;
15+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
16+
17+
return static function (ContainerConfigurator $containerConfigurator): void {
18+
$containerConfigurator->services()
19+
20+
->set('nelmio_security.cross_origin_policy_listener', CrossOriginPolicyListener::class)
21+
->args([
22+
'%nelmio_security.cross_origin_policy.coep%',
23+
'%nelmio_security.cross_origin_policy.coop%',
24+
'%nelmio_security.cross_origin_policy.corp%',
25+
])
26+
->tag('kernel.event_listener', [
27+
'event' => 'kernel.response',
28+
'method' => 'onKernelResponse',
29+
]);
30+
};

tests/App/config/config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ nelmio_security:
2121
- 'no-referrer'
2222
- 'strict-origin-when-cross-origin'
2323

24+
cross_origin_policy:
25+
enabled: true
26+
coep: credentialless
27+
coop: same-origin
28+
corp: cross-origin
29+
2430
csp:
2531
enabled: true
2632
hosts: [ ]

tests/ContentSecurityPolicy/DirectiveSetTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,12 +368,12 @@ public function provideVariousConfig(): array
368368
/**
369369
* @dataProvider provideConfigAndSignatures
370370
*
371+
* @param array<string, list<string>> $signatures
372+
*
371373
* @phpstan-param array<string, array{
372374
* enforce?: array<string, mixed>,
373375
* report?: array<string, mixed>,
374376
* }> $config
375-
*
376-
* @param array<string, list<string>> $signatures
377377
*/
378378
public function testBuildHeaderValueWithInlineSignatures(string $expected, array $config, array $signatures): void
379379
{

tests/DependencyInjection/ConfigurationTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,25 @@ public function testReferrerPolicy(): void
154154
], $config['referrer_policy']['policies']);
155155
}
156156

157+
public function testCrossOriginPolicy(): void
158+
{
159+
$config = $this->processYamlConfiguration(
160+
"cross_origin_policy:\n".
161+
" enabled: true\n".
162+
" coep: credentialless\n".
163+
" coop: same-origin-allow-popups\n".
164+
" corp: same-site\n"
165+
);
166+
167+
$this->assertIsArray($config['cross_origin_policy']);
168+
$this->assertIsString($config['cross_origin_policy']['coep']);
169+
$this->assertSame('credentialless', $config['cross_origin_policy']['coep']);
170+
$this->assertIsString($config['cross_origin_policy']['coop']);
171+
$this->assertSame('same-origin-allow-popups', $config['cross_origin_policy']['coop']);
172+
$this->assertIsString($config['cross_origin_policy']['corp']);
173+
$this->assertSame('same-site', $config['cross_origin_policy']['corp']);
174+
}
175+
157176
public function testReferrerPolicyInvalid(): void
158177
{
159178
$this->expectException(InvalidConfigurationException::class);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Nelmio SecurityBundle.
7+
*
8+
* (c) Nelmio <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Nelmio\SecurityBundle\Tests\Functional;
15+
16+
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
17+
18+
final class CrossOriginPolicyTest extends WebTestCase
19+
{
20+
public function testHasHeaders(): void
21+
{
22+
$client = static::createClient();
23+
24+
$client->request('GET', '/');
25+
26+
$this->assertResponseHeaderSame('cross-origin-embedder-policy', 'credentialless');
27+
$this->assertResponseHeaderSame('cross-origin-opener-policy', 'same-origin');
28+
$this->assertResponseHeaderSame('cross-origin-resource-policy', 'cross-origin');
29+
}
30+
}

0 commit comments

Comments
 (0)