Skip to content

Commit

Permalink
Merge pull request #22 from GoogleChromeLabs/trusted-types
Browse files Browse the repository at this point in the history
Added initial TT implementation
  • Loading branch information
henrym2 committed Sep 1, 2020
2 parents 3c321a1 + 28a9dc9 commit e6a5713
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 20 deletions.
13 changes: 13 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function getConfigTreeBuilder()
->append($this->getCOEP())
->append($this->getCOOP())
->append($this->getFetchmetaData())
->append($this->getTrustedTypes())
->end()

->arrayNode('paths')
Expand All @@ -36,6 +37,7 @@ public function getConfigTreeBuilder()
->append($this->getCOEP())
->append($this->getCOOP())
->append($this->getFetchmetaData())
->append($this->getTrustedTypes())
->end()
->end()
;
Expand Down Expand Up @@ -78,6 +80,17 @@ private function getFetchmetaData()
return $node;
}

private function getTrustedTypes()
{
$node = new ArrayNodeDefinition('trusted_types');
$node->children()
->booleanNode('active')->end()
->arrayNode('policies')->prototype('scalar')->end()->end()
->arrayNode('require_for')->prototype('scalar')->end()
->end();
return $node;
}

private function getReportConfig()
{
$node = new ScalarNodeDefinition('report_uri');
Expand Down
79 changes: 79 additions & 0 deletions EventSubscriber/TrustedTypesSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace Ise\WebSecurityBundle\EventSubscriber;

use Ise\WebSecurityBundle\Options\ConfigProviderInterface;
use Ise\WebSecurityBundle\Options\ContextChecker;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
* TrustedTypesSubscriber
* Request subscriber for implementing the Trusted Types CSP policy. This subscriber will work with already defined CSP policies.
*/
class TrustedTypesSubscriber implements EventSubscriberInterface
{
private $configProvider;
private $context;
private $logger;
private $policyIssueMessage = "Trusted types policy already defined in CSP header in request from %s. This may cause unexpected behaviour.";

public function __construct(ConfigProviderInterface $configProvider, ContextChecker $context, LoggerInterface $logger)
{
$this->configProvider = $configProvider;
$this->context = $context;
$this->logger = $logger;
}

public static function getSubscribedEvents()
{
return [
KernelEvents::RESPONSE => [
['responseEvent', -512],
]
];
}

public function responseEvent(ResponseEvent $event)
{
$response = $event->getResponse();
$request = $event->getRequest();

$options = $this->configProvider->getPathConfig($request);
//Check is trusted types is active, if not then leave handler
if (!$options['trusted_types']['active']) {
return;
}
//Check if CSP header is set
$this->context->checkSecure($request, 'Trusted types');
$headerSet = $response->headers->has("Content-Security-Policy");
//If CSP header is set, pull it and append a ';' separator, else set an empty prefix.
$headerPrefix = $headerSet ? $response->headers->get("Content-Security-Policy").';' : '';
//Check if trusted types policy is set. If so, print unexpected behaviour error
if (strpos($headerPrefix, 'trusted-types')) {
$policyIssue = sprintf($this->policyIssueMessage, $request->getUri());
$this->logger->log(0, $policyIssue, ['CSP header' => $headerPrefix]);
}

//Set trusted types CSP policy, and append it to the current policy if one exists
$response->headers->set("Content-Security-Policy", $this->constructTrustedTypesHeader($options['trusted_types'], $headerPrefix));
}

/**
* constructTrustedTypesHeader method constructs the CSP policy for trusted types. If a CSP policy already exists, the trusted types policy is appended to it.
*
* @param Array $options
* @param String $headerSet
* @return String
*/
private function constructTrustedTypesHeader($options, $headerPrefix)
{
$policies = "trusted-types ".implode(" ", $options['policies']);
$requireFor = "require-trusted-types-for ".implode(" ", array_map(function ($value) {
return sprintf('\'%s\'', $value);
}, $options['require_for']));
return sprintf("%s %s; %s;", $headerPrefix, $policies, $requireFor);
}
}
24 changes: 8 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,16 @@ including:

# 🖥️ Usage

>WIP, package not currently published.
To install the bundle on a project, add the following lines to your composer.json

```json
"require": {
"ise/web-security-bundle": "dev-main"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/GoogleChromeLabs/IseWebSecurityBundle.git"
}
]
```
Install the package from Packagist:

`composer require googlechromelabs/ise-web-security-bundle`

Due to a lack of Symfony Flex recipe to do so automatically. In your projects `/config/packages` folder, create `ise_web_security.yaml` and populate it with the yaml config detailed below.

## Config

More Config details can be found [here](https://github.com/GoogleChromeLabs/IseWebSecurityBundle/wiki/Configuration)

>WIP, Config will change over time

The config within your Symfony project will control how the bundle works in your Application.
Below, you will find an example config for the current state of the project that will activate
Expand All @@ -58,6 +45,11 @@ ise_web_security:
'^/admin':
fetch_metadata:
allowed_endpoints: ['/images']
trusted_types:
active: true
polices: ['foo', 'bar']
require_for: ['script', 'style']

```

## Wiki
Expand Down
6 changes: 5 additions & 1 deletion Resources/config/presets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ presets:
fetch_metadata:
active: false
allowed_endpoints: []
trusted_types:
active: false
same_site_restricted:
coop:
active: true
Expand All @@ -29,4 +31,6 @@ presets:
policy: 'require-corp'
fetch_metadata:
active: true
allowed_endpoints: []
allowed_endpoints: []
trusted_types:
active: false
7 changes: 7 additions & 0 deletions Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ services:

Ise\WebSecurityBundle\EventSubscriber\ResponseSubscriber: '@ise_coop_coep.subscriber'

Ise\WebSecurityBundle\EventSubscriber\TrustedTypesSubscriber: '@ise_trusted_types.subscriber'

ise_trusted_types.subscriber:
class: Ise\WebSecurityBundle\EventSubscriber\TrustedTypesSubscriber
arguments:
$configProvider: '@Ise\WebSecurityBundle\Options\ConfigProvider'

ise_config.provider:
class: Ise\WebSecurityBundle\Options\ConfigProvider
arguments:
Expand Down
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<whitelist>
<directory suffix=".php">Policies/</directory>
<directory suffix=".php">Options/</directory>
<directory suffix=".php">EventSubscriber/</directory>
</whitelist>
</filter>
<logging>
Expand Down
86 changes: 86 additions & 0 deletions tests/COOPCOEPTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace Ise\WebSecurityBundle\Tests;

use Ise\WebSecurityBundle\EventSubscriber\ResponseSubscriber;
use Ise\WebSecurityBundle\Options\ConfigProvider;
use Ise\WebSecurityBundle\Options\ContextChecker;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class COOPCOEPTest extends TestCase
{
private $default = [
"coop" => [
"active" => true,
"policy" => 'same-origin'
],
"coep" => [
"active" => true,
"policy" => 'require-corp'
]
];

private $coop = "same-origin";
private $coep = "require-corp";

public function testCOOP()
{
$logger = $this->getMockBuilder(LoggerInterface::class)
->disableOriginalConstructor()
->getMock();
$context = new ContextChecker($logger);
$requestSub = new ResponseSubscriber(
new ConfigProvider($this->default, []),
$context
);

$kernel = $this->getMockBuilder(HttpKernelInterface::class)
->disableOriginalConstructor()
->getMock();

$request = Request::create('/test');
$res = new ResponseEvent(
$kernel,
$request,
HttpKernelInterface::MASTER_REQUEST,
new Response()
);

$result = $requestSub->responseEvent($res);
$this->assertNull($result);
$this->assertEquals($res->getResponse()->headers->get('Cross-Origin-Opener-Policy'), $this->coop);
}

public function testCOEP()
{
$logger = $this->getMockBuilder(LoggerInterface::class)
->disableOriginalConstructor()
->getMock();
$context = new ContextChecker($logger);
$requestSub = new ResponseSubscriber(
new ConfigProvider($this->default, []),
$context
);

$kernel = $this->getMockBuilder(HttpKernelInterface::class)
->disableOriginalConstructor()
->getMock();

$request = Request::create('/test');
$res = new ResponseEvent(
$kernel,
$request,
HttpKernelInterface::MASTER_REQUEST,
new Response()
);

$result = $requestSub->responseEvent($res);
$this->assertNull($result);
$this->assertEquals($res->getResponse()->headers->get('Cross-Origin-Embedder-Policy'), $this->coep);
}
}
35 changes: 32 additions & 3 deletions tests/FetchMetadataPolicyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Ise\WebSecurityBundle\Options\ConfigProvider;
use Ise\WebSecurityBundle\Policies\FetchMetadataDefaultPolicy;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

Expand Down Expand Up @@ -36,22 +37,50 @@ public function testFetchMetaDataSubscriber()
$logger
);

$req = Request::create(
'/blog'
$kernel = $this->getMockBuilder(HttpKernelInterface::class)
->disableOriginalConstructor()
->getMock();

$request = Request::create('/test');
$res = new RequestEvent(
$kernel,
$request,
HttpKernelInterface::MASTER_REQUEST
);

$result = $requestSubscriber->requestEvent($res);
$this->assertNull($result);
}

public function testSubscriberRejection()
{
$fetchMetaPolicy = new FetchMetadataDefaultPolicy([]);

$logger = $this->getMockBuilder(LoggerInterface::class)
->disableOriginalConstructor()
->getMock();

$requestSubscriber = new FetchMetadataRequestSubscriber(
new FetchMetadataPolicyProvider,
new ConfigProvider($this->defaults, []),
$logger
);

$kernel = $this->getMockBuilder(HttpKernelInterface::class)
->disableOriginalConstructor()
->getMock();

$request = Request::create('/test');
$request = Request::create('/test', 'PUT');
$request->headers->set('sec-fetch-dest', 'object');
$request->headers->set('sec-fetch-site', 'cross-origin');
$res = new RequestEvent(
$kernel,
$request,
HttpKernelInterface::MASTER_REQUEST
);

$result = $requestSubscriber->requestEvent($res);
$this->assertEquals($res->getResponse()->getStatusCode(), Response::HTTP_UNAUTHORIZED);
$this->assertNull($result);
}

Expand Down
Loading

0 comments on commit e6a5713

Please sign in to comment.