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 use an interface for mocking
Browse files Browse the repository at this point in the history
  • Loading branch information
ancarda committed Sep 7, 2019
1 parent 73d415b commit 0537541
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 264 deletions.
189 changes: 61 additions & 128 deletions src/ContentSecurityPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Ancarda\Security\Header;

use Exception;

/**
* Helper class to build a simple Content-Security-Policy header
*
Expand Down Expand Up @@ -34,18 +36,8 @@
* @author Mark Dain <mark@markdain.net>
* @license https://choosealicense.com/licenses/mit/ (MIT License)
*/
final class ContentSecurityPolicy implements HeaderInterface
final class ContentSecurityPolicy implements ContentSecurityPolicyInterface
{
/**
* Returns the name of this header's when expressed over the network
*
* @return string
*/
public function name(): string
{
return 'Content-Security-Policy';
}

/**
* Array of domains, used to track what endpoints are whitelisted for what.
*
Expand All @@ -70,196 +62,137 @@ public function name(): string
'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
Original file line number Diff line number Diff line change
@@ -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 0537541

Please sign in to comment.