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 authored Sep 1, 2020

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 3c321a1 + 28a9dc9 commit e6a5713
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
@@ -25,6 +25,7 @@ public function getConfigTreeBuilder()
->append($this->getCOEP())
->append($this->getCOOP())
->append($this->getFetchmetaData())
->append($this->getTrustedTypes())
->end()

->arrayNode('paths')
@@ -36,6 +37,7 @@ public function getConfigTreeBuilder()
->append($this->getCOEP())
->append($this->getCOOP())
->append($this->getFetchmetaData())
->append($this->getTrustedTypes())
->end()
->end()
;
@@ -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');
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
@@ -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
@@ -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
6 changes: 5 additions & 1 deletion Resources/config/presets.yaml
Original file line number Diff line number Diff line change
@@ -20,6 +20,8 @@ presets:
fetch_metadata:
active: false
allowed_endpoints: []
trusted_types:
active: false
same_site_restricted:
coop:
active: true
@@ -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
@@ -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:
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
<whitelist>
<directory suffix=".php">Policies/</directory>
<directory suffix=".php">Options/</directory>
<directory suffix=".php">EventSubscriber/</directory>
</whitelist>
</filter>
<logging>
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
@@ -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;

@@ -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);
}

Loading

0 comments on commit e6a5713

Please sign in to comment.