Skip to content

Commit

Permalink
[FEATURE] Introduce SecurityHeadersResponseSubscriber (#67)
Browse files Browse the repository at this point in the history
A new onResponse subscriber `SecurityHeadersResponseSubscriber` is
introduced which sends generic headers and CSP related headers if
configured in a project's template.yaml.
  • Loading branch information
andreaskienast committed Jun 22, 2021
1 parent e455f61 commit 8961303
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 0 deletions.
21 changes: 21 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,27 @@ public function getConfigTreeBuilder()
->end()
->end()
->end()
->arrayNode('security')->addDefaultsIfNotSet()
->children()
->arrayNode('headers')
->useAttributeAsKey('name')
->normalizeKeys(false)
->scalarPrototype()->end()
->end()
->arrayNode('content_security_policy')->addDefaultsIfNotSet()
->children()
->booleanNode('report_only')
->defaultValue(true)
->end()
->arrayNode('rules')
->useAttributeAsKey('name')
->normalizeKeys(false)
->scalarPrototype()->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/TemplateExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function load(array $configs, ContainerBuilder $container): void

$container->setParameter('t3g.template.config', $config);
$container->setParameter('t3g.template.config.menu.class', $config['application']['menu']['class']);
$container->setParameter('t3g.template.config.security', $config['application']['security']);

$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.yaml');
Expand Down
83 changes: 83 additions & 0 deletions src/EventSubscriber/SecurityHeadersResponseSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

/*
* This file is part of the package t3g/symfony-template-bundle.
*
* For the full copyright and license information, please read the
* LICENSE file that was distributed with this source code.
*/

namespace T3G\Bundle\TemplateBundle\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class SecurityHeadersResponseSubscriber implements EventSubscriberInterface
{
/**
* @var array{headers: array<string, string>, content_security_policy: array{report_only: bool, rules: array<string, string>}} $configuration
*/
private array $configuration;

/**
* @param array{headers: array<string, string>, content_security_policy: array{report_only: bool, rules: array<string, string>}} $configuration
*/
public function __construct(array $configuration)
{
$this->configuration = $configuration;
}

/**
* @return array<string, string>
*/
public static function getSubscribedEvents(): array
{
return [
KernelEvents::RESPONSE => 'onResponse',
];
}

public function onResponse(ResponseEvent $event): void
{
$response = $event->getResponse();

$this->applyHeaders($response);
$this->applyContentSecurityPolicyRules($response);
}

private function applyHeaders(Response $response): void
{
$headers = array_change_key_case($this->configuration['headers'], CASE_LOWER);
if (!isset($headers['x-frame-options'])) {
// Symfony DI configuration doesn't allow to pre-define a scalar within prototypes *and* have that always
// available, thus we're adding it here.
$headers['x-frame-options'] = 'sameorigin';
}

$response->headers->add($headers);
}

private function applyContentSecurityPolicyRules(Response $response): void
{
$contentSecurityPolicy = $this->configuration['content_security_policy'];
if ([] === $contentSecurityPolicy['rules']) {
return;
}

$ruleSet = [];
foreach ($contentSecurityPolicy['rules'] as $ruleKey => $ruleValue) {
$ruleSet[] = sprintf('%s %s', $ruleKey, $ruleValue);
}
$policy = implode('; ', $ruleSet);

$headerName = $contentSecurityPolicy['report_only']
? 'Content-Security-Policy-Report-Only'
: 'Content-Security-Policy';
$response->headers->set($headerName, $policy);
$response->headers->set(sprintf('X-%s', $headerName), $policy);
}
}
5 changes: 5 additions & 0 deletions src/Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ services:
- '../../../src/DependencyInjection/'
- '../../../src/Tests/'

T3G\Bundle\TemplateBundle\EventSubscriber\SecurityHeadersResponseSubscriber:
class: T3G\Bundle\TemplateBundle\EventSubscriber\SecurityHeadersResponseSubscriber
arguments:
$configuration: '%t3g.template.config.security%'

T3G\Bundle\TemplateBundle\Twig\Extension\GlobalVariablesExtension:
arguments: ["%t3g.template.config%"]
tags:
Expand Down
2 changes: 2 additions & 0 deletions tests/Unit/DependencyInjection/TemplateExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public function testDefault()
$this->assertEquals('https://typo3.com/legal-notice', $config['routes']['legal']);
$this->assertEquals('https://jira.typo3.com/servicedesk/customer/portal/2', $config['routes']['feedback']);
$this->assertEquals('T3G\Bundle\TemplateBundle\Menu\MenuBuilder', $config['menu']['class']);
$this->assertEquals([], $config['security']['headers']);
$this->assertEquals(['report_only' => true, 'rules' => []], $config['security']['content_security_policy']);
$this->assertEquals(false, $config['assets']['encore_entrypoint']);
$this->assertEquals(false, $config['theme']['use_logo']);
$this->assertEquals('lg', $config['theme']['navbar_breakpoint']);
Expand Down

0 comments on commit 8961303

Please sign in to comment.