Skip to content
This repository has been archived by the owner on Sep 17, 2020. It is now read-only.

Commit

Permalink
Rewrite classes to have interfaces
Browse files Browse the repository at this point in the history
This commit makes a large change to the structure of this package. By
changing FooHeader class to implement FooHeaderInterface, downstream
consumers can mock FooHeader and can type hint based on the interface
rather than the implementation
  • Loading branch information
ancarda committed Sep 7, 2019
1 parent c936aec commit 1b0fdd2
Show file tree
Hide file tree
Showing 16 changed files with 469 additions and 252 deletions.
185 changes: 61 additions & 124 deletions src/ContentSecurityPolicy.php
Expand Up @@ -4,18 +4,15 @@

namespace Ancarda\Security\Header;

use Exception;

/**
* Helper class to build a simple Content-Security-Policy header
*
* To use this class, instantiate it, and use the with() methods to build up a
* whitelist of where stylesheets and scripts can come from, as well as where
* connections (XMLHttpRequest and WebSocket) are permitted to go to.
*
* This class is immutable, so the `with` methods return a new instance of this
* class with the additions you requested. Any function here that returns an
* instance of ContentSecurityPolicy isn't making any changes to the current
* object.
*
* You could use this library as follows:
*
* $csp = new ContentSecurityPolicy();
Expand All @@ -36,11 +33,10 @@
*
* Everything defaults to denied.
*
* @package Ancarda_Security_Headers
* @author Mark Dain <mark@markdain.net>
* @license https://choosealicense.com/licenses/mit/ (MIT License)
*/
final class ContentSecurityPolicy
final class ContentSecurityPolicy implements ContentSecurityPolicyInterface
{
/**
* Array of domains, used to track what endpoints are whitelisted for what.
Expand All @@ -66,196 +62,137 @@ final class ContentSecurityPolicy
'images' => false,
];

/**
* String containing a nonce (temporary) value.
*
* @var string
*/
/** @var string */
private $nonce = null;

/**
* Creates a new instance of the Content Security Policy builder class.
*
* This function will randomly generate a nonce value using random_bytes().
*
* @throws Exception If a nonce cannot be generated
*/
public function __construct()
{
$this->nonce = bin2hex(random_bytes(16));
}

/**
* Returns the Content Security Policy nonce value that allows inline content to
* be rendered and executed.
* Returns the name of this header's when expressed over the network
*
* @return string
*/
public function getNonce(): string
public function name(): string
{
return $this->nonce;
return 'Content-Security-Policy';
}

/**
* Returns the compiled Content Security Policy header value.
*
* @return string
*/
public function compile(): string
public function getNonce(): string
{
$out = 'default-src \'none\'; ';

$script = 'script-src ';
if ($this->self['scripts']) {
$script .= '\'self\' ';
}
foreach ($this->domains['scripts'] as $s) {
$script .= $s . ' ';
}
$script .= '\'nonce-' . $this->nonce . '\'';
$out .= trim($script) . '; ';

$style = 'style-src ';
if ($this->self['stylesheets']) {
$style .= '\'self\' ';
}
foreach ($this->domains['stylesheets'] as $s) {
$style .= $s . ' ';
}
$style .= '\'nonce-' . $this->nonce . '\'';
$out .= trim($style) . '; ';

if ($this->self['connect']) {
$out .= 'connect-src \'self\'; ';
}

$images = 'img-src ';
if ($this->self['images']) {
$images .= '\'self\' ';
}
foreach ($this->domains['images'] as $s) {
$images .= $s . ' ';
}
$images .= '\'nonce-' . $this->nonce . '\'';
$out .= trim($images) . '; ';

return trim($out);
return $this->nonce;
}

/**
* Sets the nonce value used in this policy.
*
* A suitable, random nonce is automatically generated by the constructor, but
* may be changed by this method. The nonce should be at-least 32 characters
* long.
*
* @param string $nonce Random nonce, at-least 32 characters
* @return ContentSecurityPolicy
*/
public function withNonce(string $nonce): self
public function withNonce(string $nonce): ContentSecurityPolicyInterface
{
$clone = clone $this;
$clone->nonce = $nonce;
return $clone;
}

/**
* Whitelists executing scripts from the specified domain.
*
* @param string $domain Domain to add
* @return ContentSecurityPolicy
*/
public function withScriptsFromDomain(string $domain): self
public function withScriptsFromDomain(string $domain): ContentSecurityPolicyInterface
{
$clone = clone $this;
$clone->whitelistDomain('scripts', $domain);
return $clone;
}

/**
* Whitelists executing scripts on the same domain the policy is active on.
*
* @return ContentSecurityPolicy
*/
public function withScriptsFromSelf(): self
public function withScriptsFromSelf(): ContentSecurityPolicyInterface
{
$clone = clone $this;
$clone->self['scripts'] = true;
return $clone;
}

/**
* Whitelists connecting (XMLHttpRequest and WebSockets) to same domain the
* policy is active on.
*
* @return ContentSecurityPolicy
*/
public function withConnectToSelf(): self
public function withConnectToSelf(): ContentSecurityPolicyInterface
{
$clone = clone $this;
$clone->self['connect'] = true;
return $clone;
}

/**
* Whitelists rendering stylesheets from the specified domain.
*
* @param string $domain Domain to add
* @return ContentSecurityPolicy
*/
public function withStylesheetsFromDomain(string $domain): self
public function withStylesheetsFromDomain(string $domain): ContentSecurityPolicyInterface
{
$clone = clone $this;
$clone->whitelistDomain('stylesheets', $domain);
return $clone;
}

/**
* Whitelists rendering stylesheets on the same domain the policy is active on.
*
* @return ContentSecurityPolicy
*/
public function withStylesheetsFromSelf(): self
public function withStylesheetsFromSelf(): ContentSecurityPolicyInterface
{
$clone = clone $this;
$clone->self['stylesheets'] = true;
return $clone;
}

/**
* Whitelists displaying images from the specified domain.
*
* @param string $domain Domain to add
* @return ContentSecurityPolicy
*/
public function withImagesFromDomain(string $domain): self
public function withImagesFromDomain(string $domain): ContentSecurityPolicyInterface
{
$clone = clone $this;
$clone->whitelistDomain('images', $domain);
return $clone;
}

/**
* Whitelists displaying images on the same domain the policy is active on.
*
* @return ContentSecurityPolicy
*/
public function withImagesFromSelf(): self
public function withImagesFromSelf(): ContentSecurityPolicyInterface
{
$clone = clone $this;
$clone->self['images'] = true;
return $clone;
}

/**
* Adds a domain to a whitelist.
*
* @param string $bucket The whitelist in $domains.
* @param string $domain The domain to add.
* @return void
*/
private function whitelistDomain(string $bucket, string $domain)
private function whitelistDomain(string $bucket, string $domain): void
{
if (!in_array($domain, $this->domains[$bucket], true)) {
$this->domains[$bucket][] = $domain;
}
}

public function compile(): string
{
$out = 'default-src \'none\'; ';

$script = 'script-src ';
if ($this->self['scripts']) {
$script .= '\'self\' ';
}
foreach ($this->domains['scripts'] as $s) {
$script .= $s . ' ';
}
$script .= '\'nonce-' . $this->nonce . '\'';
$out .= trim($script) . '; ';

$style = 'style-src ';
if ($this->self['stylesheets']) {
$style .= '\'self\' ';
}
foreach ($this->domains['stylesheets'] as $s) {
$style .= $s . ' ';
}
$style .= '\'nonce-' . $this->nonce . '\'';
$out .= trim($style) . '; ';

if ($this->self['connect']) {
$out .= 'connect-src \'self\'; ';
}

$images = 'img-src ';
if ($this->self['images']) {
$images .= '\'self\' ';
}
foreach ($this->domains['images'] as $s) {
$images .= $s . ' ';
}
$images .= '\'nonce-' . $this->nonce . '\'';
$out .= trim($images) . '; ';

return trim($out);
}
}
108 changes: 108 additions & 0 deletions src/ContentSecurityPolicyInterface.php
@@ -0,0 +1,108 @@
<?php

declare(strict_types=1);

namespace Ancarda\Security\Header;

/**
* Helper class to build a simple Content-Security-Policy header
*
* To use this class, instantiate it, and use the with() methods to build up a
* whitelist of where stylesheets and scripts can come from, as well as where
* connections (XMLHttpRequest and WebSocket) are permitted to go to.
*
* You could use this library as follows:
*
* $csp = new ContentSecurityPolicy();
* $csp = $csp->withScriptsFromDomain('example.com');
* $csp = $csp->withStylesheetsFromSelf();
* header('Content-SecurityPolicy: ' . $csp->compile());
*
* Alternatively, you may chain these calls:
*
* $csp = new ContentSecurityPolicy();
* header('Content-Security-Policy: ' . $csp
* ->withScriptsFromDomain('example.com')
* ->withStylesheetsFromSelf()
* ->compile());
*
* Once you are done, call compile() to get the header value. You can now pass
* this to whatever method you use to set HTTP response headers.
*
* Everything defaults to denied.
*/
interface ContentSecurityPolicyInterface extends HeaderInterface
{
/**
* Returns the Content Security Policy nonce value that allows inline content to
* be rendered and executed.
*
* @return string
*/
public function getNonce(): string;

/**
* Sets the nonce value used in this policy.
*
* A suitable, random nonce is automatically generated by the constructor, but
* may be changed by this method. The nonce should be at-least 32 characters
* long.
*
* @param string $nonce Random nonce, at-least 32 characters
* @return ContentSecurityPolicyInterface
*/
public function withNonce(string $nonce): ContentSecurityPolicyInterface;

/**
* Whitelists executing scripts from the specified domain.
*
* @param string $domain Domain to add
* @return ContentSecurityPolicyInterface
*/
public function withScriptsFromDomain(string $domain): ContentSecurityPolicyInterface;

/**
* Whitelists executing scripts on the same domain the policy is active on.
*
* @return ContentSecurityPolicyInterface
*/
public function withScriptsFromSelf(): ContentSecurityPolicyInterface;

/**
* Whitelists connecting (XMLHttpRequest and WebSockets) to same domain the
* policy is active on.
*
* @return ContentSecurityPolicyInterface
*/
public function withConnectToSelf(): ContentSecurityPolicyInterface;

/**
* Whitelists rendering stylesheets from the specified domain.
*
* @param string $domain Domain to add
* @return ContentSecurityPolicyInterface
*/
public function withStylesheetsFromDomain(string $domain): ContentSecurityPolicyInterface;

/**
* Whitelists rendering stylesheets on the same domain the policy is active on.
*
* @return ContentSecurityPolicyInterface
*/
public function withStylesheetsFromSelf(): ContentSecurityPolicyInterface;

/**
* Whitelists displaying images from the specified domain.
*
* @param string $domain Domain to add
* @return ContentSecurityPolicyInterface
*/
public function withImagesFromDomain(string $domain): ContentSecurityPolicyInterface;

/**
* Whitelists displaying images on the same domain the policy is active on.
*
* @return ContentSecurityPolicyInterface
*/
public function withImagesFromSelf(): ContentSecurityPolicyInterface;
}

0 comments on commit 1b0fdd2

Please sign in to comment.