Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@
*/
->set('framework.response_securer', ResponseSecurer::class)
->args([
param('kernel.debug')
param('kernel.debug'),
param('sumo_coders_framework_core.content_security_policy'),
param('sumo_coders_framework_core.extra_content_security_policy'),
param('sumo_coders_framework_core.x_frame_options'),
param('sumo_coders_framework_core.x_content_type_options'),
])
->tag('kernel.event_listener', ['event' => 'kernel.response', 'method' => 'onKernelResponse'])

Expand Down
95 changes: 70 additions & 25 deletions docs/development/csp.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# Content security policy

The bundle sets pretty strict CSP headers on every response out-of-the-box. This prevents a large portion of XSS attacks on applications built with the framework.
The bundle sets pretty strict CSP headers on every response out-of-the-box. This prevents a large portion of XSS attacks
on applications built with the framework.

For more information, read https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
For more information,
read [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy).

## Default Content Security Policies

The default Content Security Policies are:

## Base rules
```php
// The default rule and fallback: only allow content from our own domain
"default-src 'self';" .
Expand All @@ -18,35 +23,75 @@ For more information, read https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
"script-src 'self' 'nonce-FOR725'"
```

## Extending the headers
In some cases, you might have to allow external CSS and/or JS in your project. To do so, you'll have to allow the domain on which the resource is hosted.

You can either tweak the CSP header inside a specific controller (where you already have a Response object), or add an event listener on the kernel response event and tweak the headers there (globally).
## Extending the default Content Security Policies

In some cases, you might have to allow external CSS and/or JS in your project. To do so, you'll have to allow the domain
on which the resource is hosted.

You can either tweak the CSP header inside a specific controller (where you already have a Response object). Or you can
add extra policies application wide by using a configuration file: `config/packages/sumo_coders_framework_core.yaml`.

In this file you can set the extra policies. Hereunder you can find an example to allow Google Maps.

services.yaml
```yaml
App\EventListener\ResponseListener:
tags:
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: -5 }
sumo_coders_framework_core:
extra_content_security_policy:
script-src:
- 'maps.googleapis.com'
img-src:
- "'self'"
- 'data: maps.gstatic.com'
- '*.googleapis.com'
- '*.ggpht.com'

```

ResponseListener.php
```php
<?php

namespace App\EventListener;
## Overriding the default Content Security Policies

use Symfony\Component\HttpKernel\Event\ResponseEvent;
The default Content Security Policies are initialized through configuration under the
key: `sumo_coders_framework_core.content_security_policy`. So you can overrule this by creating a
file: `config/packages/sumo_coders_framework_core.yaml`.

class ResponseListener
{
public function onKernelResponse(ResponseEvent $event)
{
$event->getResponse()->headers->set('Content-Security-Policy',
"script-src https://your-cdn.com/your-script.js",
false // Passing false here will add the new headers instead of overwrite
);
}
}
In this file you can set the directives you want like below:

```yaml
sumo_coders_framework_core:
content_security_policy:
default-src:
# Default rule: only allow content from our own domain
- "'self'"
style-src:
- "'self'"
# Allow Google Fonts
- 'https://fonts.googleapis.com'
font-src:
- "'self'"
# Allow Google Fonts
- 'https://fonts.gstatic.com'
frame-src:
# Block all iframes
- "'none'"
script-src:
- "'self'"
# Allow our jsData inline script
- "'nonce-FOR725'"

```



# X-Frame-Options
See [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) for more information.

Can be configured by setting `sumo_coders_framework_core.x-frame-options`. The default is `deny`. If you set an empty
string the header won't be added.



# X-Content-Type-Options
See [X-Content-Type-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options) for more information.

Can be configured by setting `sumo_coders_framework_core.x-content-type-options`. The default is `nosniff`. If you set
an empty string the header won't be added.
51 changes: 51 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace SumoCoders\FrameworkCoreBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('sumo_coders_framework_core');

$treeBuilder->getRootNode()
->children()
->variableNode('content_security_policy')
->defaultValue([
'default-src' => [
"'self'", // Default rule: only allow content from our own domain
],
'style-src' => [
"'self'",
],
'font-src' => [
"'self'",
],
'frame-src' => [
"'none'", // Block all iframes
],
'script-src' => [
"'self'",
"'nonce-FOR725'", // Allow our jsData inline script
],
])
->end()
->variableNode('extra_content_security_policy')
->defaultValue([])
->end()
->enumNode('x_frame_options')
->values(['', 'deny', 'sameorigin'])
->defaultValue('deny')
->end()
->enumNode('x_content_type_options')
->values(['', 'nosniff'])
->defaultValue('nosniff')
->end()
->end();

return $treeBuilder;
}
}
22 changes: 22 additions & 0 deletions src/DependencyInjection/SumoCodersFrameworkCoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,27 @@ public function load(array $configs, ContainerBuilder $container)
new FileLocator(__DIR__ . '/../../config')
);
$loader->load('services.php');

$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);

$container->setParameter(
'sumo_coders_framework_core.content_security_policy',
$config['content_security_policy']
);
$container->setParameter(
'sumo_coders_framework_core.extra_content_security_policy',
$config['extra_content_security_policy']
);

$container->setParameter(
'sumo_coders_framework_core.x_frame_options',
$config['x_frame_options']
);

$container->setParameter(
'sumo_coders_framework_core.x_content_type_options',
$config['x_content_type_options']
);
}
}
59 changes: 49 additions & 10 deletions src/EventListener/ResponseSecurer.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,23 @@
class ResponseSecurer
{
private bool $isDebug;
private array $cspDirectives;
private array $extraCspDirectives;
private string $xFrameOptions;
private string $xContentTypeOptions;

public function __construct(bool $isDebug)
{
public function __construct(
bool $isDebug,
array $cspDirectives,
array $extraCspDirectives,
string $xFrameOptions,
string $xContentTypeOptions
) {
$this->isDebug = $isDebug;
$this->cspDirectives = $cspDirectives;
$this->extraCspDirectives = $extraCspDirectives;
$this->xFrameOptions = $xFrameOptions;
$this->xContentTypeOptions = $xContentTypeOptions;
}

/**
Expand All @@ -25,14 +38,40 @@ public function onKernelResponse(ResponseEvent $event)
* We only send the security headers when we're not in dev mode
*/
if (!$this->isDebug) {
$event->getResponse()->headers->set('Content-Security-Policy',
"default-src 'self';" . // Default rule: only allow content from our own domain
"frame-src 'none';" . // Block all iframes
"script-src 'self' 'nonce-FOR725'" // Allow our jsData inline script
);

$event->getResponse()->headers->set('X-Frame-Options', 'deny');
$event->getResponse()->headers->set('X-Content-Type-Options', 'nosniff');
if (!empty($this->cspDirectives)) {
$event->getResponse()->headers->set('Content-Security-Policy', $this->buildCSPDirectiveString());
}

if ($this->xFrameOptions !== '') {
$event->getResponse()->headers->set('X-Frame-Options', $this->xFrameOptions);
}

if ($this->xContentTypeOptions !== '') {
$event->getResponse()->headers->set('X-Content-Type-Options', $this->xContentTypeOptions);
}
}
}

private function buildCSPDirectiveString(): string
{
$cspDirectives = $this->cspDirectives;

if (!empty($this->extraCspDirectives)) {
foreach ($this->extraCspDirectives as $directive => $policies) {
if (array_key_exists($directive, $cspDirectives)) {
$cspDirectives[$directive] = array_unique(array_merge($cspDirectives[$directive], $policies));
} else {
$cspDirectives[$directive] = $policies;
}
}
}

$policyDirectivesString = '';

foreach ($cspDirectives as $directive => $policies) {
$policyDirectivesString .= $directive . ' ' . implode(' ', $policies) . ';' . "\n";
}

return $policyDirectivesString;
}
}