Skip to content

Commit

Permalink
💥 refac
Browse files Browse the repository at this point in the history
- switched csp lib (support loading config from json file)
- added tests
- updated readme

Signed-off-by: Bruno Meilick <b@bnomei.com>
  • Loading branch information
bnomei committed Sep 1, 2019
1 parent 86f51b1 commit 23e6612
Show file tree
Hide file tree
Showing 41 changed files with 4,010 additions and 1,358 deletions.
247 changes: 170 additions & 77 deletions classes/SecurityHeaders.php
Original file line number Diff line number Diff line change
@@ -1,112 +1,205 @@
<?php

declare(strict_types=1);

namespace Bnomei;

use Kirby\Data\Json;
use Kirby\Data\Yaml;
use Kirby\Toolkit\A;
use Phpcsp\Security\ContentSecurityPolicyHeaderBuilder;
use function filemtime;
use Kirby\Toolkit\F;
use Kirby\Toolkit\Mime;
use ParagonIE\CSPBuilder\CSPBuilder;
use function header;

class SecurityHeaders
final class SecurityHeaders
{
private static function enabled()
{
$inPanel = static::isPanel() ? option('bnomei.securityheaders.enabled.panel') : true;
return option('bnomei.securityheaders.enabled') && $inPanel && !static::isWebpack() && !static::isLocalhost();
}
/*
* @var array
*/
private $options;

/*
* @var ParagonIE\CSPBuilder\CSPBuilder
*/
private $cspBuilder;

/*
* @var array
*/
private $nonces;

private static function isPanel()
public function __construct(array $options = [])
{
return strpos(
kirby()->request()->url()->toString(),
kirby()->urls()->panel
) !== false;
$defaults = [
'debug' => option('debug'),
'loader' => option('bnomei.securityheaders.loader'),
'enabled' => option('enabled', !kirby()->system()->isLocal()),
'headers' => option('bnomei.securityheaders.headers'),
'panelnonces' => method_exists(kirby()->system(), 'nonces') ? kirby()->system()->nonces() : [],
'setter' => option('bnomei.securityheaders.setter'),
];
$this->options = array_merge($defaults, $options);
$this->nonces = [];

foreach ($this->options as $key => $call) {
if (is_callable($call) && in_array($key, ['loader', 'enabled', 'headers'])) {
$this->options[$key] = $call();
}
}
}

public static function headers($headers)
/**
* @return array|misc
*/
public function option(?string $key = null)
{
if (!static::enabled()) {
return;
}
$options = option('bnomei.securityheaders.headers', []);
$optionsKV = array_map(function ($k, $v) {
return [
'name' => $k,
'value' => $v,
];
}, array_keys($options), $options);
$headers = array_merge($headers, $optionsKV);

foreach ($headers as $h) {
header(sprintf('%s: %s', $h['name'], $h['value']));
if ($key) {
return A::get($this->options, $key);
}
return $this->options;
}

private static $nonces = null;
public static function nonce($string, $value = null)
/**
* @return string|null
*/
public function getNonce(string $key): ?string
{
if (!static::$nonces) {
static::$nonces = [];
}
if ($value && is_string($value)) {
static::$nonces[$string] = $value;
}
return A::get(static::$nonces, $string);
return A::get($this->nonces, $key);
}

private static function isWebpack()
/**
* @param string
*/
public function setNonce(string $key): string
{
return !!(isset($_SERVER['HTTP_X_FORWARDED_FOR'])
&& $_SERVER['HTTP_X_FORWARDED_FOR'] == 'webpack');
$nonceArr = [$key, time(), filemtime(__FILE__), kirby()->roots()->assets()];
shuffle($nonceArr);
$nonce = 'nonce-' . base64_encode(sha1(implode('', $nonceArr)));

$this->nonces[$key] = $nonce;
return $nonce;
}

private static function isLocalhost()
/**
* @return mixed
*/
public function csp()
{
return !array_key_exists('REMOTE_ADDR', $_SERVER) || in_array($_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1'));
return $this->cspBuilder;
}

public static function apply()
/**
* @param null $data
* @return CSPBuilder
*/
public function load($data = null): CSPBuilder
{
if (!static::enabled()) {
return;
if (is_null($data)) {
$data = $this->option('loader');
}

// https://github.com/Martijnc/php-csp
$policy = new ContentSecurityPolicyHeaderBuilder();

$csp = option('bnomei.securityheaders.csp', []);
if (!$csp) {
$sourcesetID = kirby()->site()->title()->value();
$policy->defineSourceSet($sourcesetID, [kirby()->site()->url()]);

$directives = [
ContentSecurityPolicyHeaderBuilder::DIRECTIVE_DEFAULT_SRC,
ContentSecurityPolicyHeaderBuilder::DIRECTIVE_STYLE_SRC,
ContentSecurityPolicyHeaderBuilder::DIRECTIVE_SCRIPT_SRC,
ContentSecurityPolicyHeaderBuilder::DIRECTIVE_IMG_SRC,
ContentSecurityPolicyHeaderBuilder::DIRECTIVE_FONT_SRC,
ContentSecurityPolicyHeaderBuilder::DIRECTIVE_CONNECT_SRC,
];
foreach ($directives as $d) {
$policy->addSourceSet($d, $sourcesetID);
if (is_string($data) && F::exists($data)) {
$mime = F::mime($data);
$data = F::read($data);
if (in_array($mime, A::get(Mime::types(), 'json'))) {
$data = Json::decode($data);
} elseif (A::get(Mime::types(), 'yaml') && in_array($mime, A::get(Mime::types(), 'yaml'))) {
// TODO: kirby has no mime yaml yet. pending issue.
$data = Yaml::decode($data);
}
} elseif (is_callable($csp)) {
$policy = $csp($policy);
}
if (is_array($data)) {
$this->cspBuilder = CSPBuilder::fromArray($data);
} else {
$this->cspBuilder = new CSPBuilder();
}

$nc = ['loadjs.min.js', 'loadjs.min.js-fn', 'webfontloader.js']; // https://github.com/bnomei/kirby3-htmlhead
$nc = array_merge($nc, option('bnomei.securityheaders.nonces', []));
foreach ($nc as $id) {
$nonceArr = [$id, time(), filemtime(__FILE__), kirby()->roots()->assets()];
shuffle($nonceArr);
$nonce = 'nonce-' . base64_encode(sha1(implode('', $nonceArr)));
static::nonce($id, $nonce);
$policy->addNonce(ContentSecurityPolicyHeaderBuilder::DIRECTIVE_SCRIPT_SRC, $nonce);
// add panel nonces
$panelnonces = $this->option('panelnonces');
foreach ($panelnonces as $nonce) {
// TODO: kirby has no panel nonces yet. pending issue.
$this->cspBuilder->nonce('script-src', $nonce);
}
foreach (option('bnomei.securityheaders.hashes', []) as $h) {
$policy->addHash(ContentSecurityPolicyHeaderBuilder::HASH_SHA_256, $h);
// hash(ContentSecurityPolicyHeaderBuilder::HASH_SHA_256, $script, true)

return $this->cspBuilder;
}

/**
*
*/
public function applySetter()
{
// additional setters
$csp = $this->option('setter');
if (is_callable($csp)) {
$csp($this);
}
}

/**
* @return bool
*/
public function sendHeaders(): bool
{
if ($this->option('debug') || $this->option('enabled') !== true) {
return false;
}

// from config
$headers = $this->option('headers');
foreach ($headers as $key => $value) {
header($key . ': ' . $value);
}

// from cspbuilder
if($this->cspBuilder) {
$this->cspBuilder->sendCSPHeader();
}
return true;
}

/**
* @param string $filepath
* @return bool
*/
public function saveApache(string $filepath): bool
{
$this->cspBuilder->saveSnippet($filepath, CSPBuilder::FORMAT_APACHE);
return F::exists($filepath);
}

/**
* @param string $filepath
* @return bool
*/
public function saveNginx(string $filepath): bool
{
$this->cspBuilder->saveSnippet($filepath, CSPBuilder::FORMAT_NGINX);
return F::exists($filepath);
}

/*
* @var SecurityHeaders
*/
private static $singleton;

/**
* @param array $options
* @return SecurityHeaders
* @codeCoverageIgnore
*/
public static function singleton(array $options = []): SecurityHeaders
{
if (self::$singleton) {
return self::$singleton;
}

$sec = new SecurityHeaders($options);
$sec->load();
$sec->applySetter();
self::$singleton = $sec;

static::headers($policy->getHeaders(true));
return self::$singleton;
}
}
15 changes: 11 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "bnomei/kirby3-security-headers",
"type": "kirby-plugin",
"version": "1.1.4",
"version": "2.0.0",
"license": "MIT",
"description": "Kirby 3 Plugin for easier Security Headers setup",
"authors": [
Expand All @@ -16,7 +16,14 @@
"kirby3-plugin",
"content-security-policy",
"security-headers",
"csp"
"csp",
"nonce",
"nonces",
"hash",
"apache",
"nginx",
"json",
"yaml"
],
"autoload": {
"psr-4": {
Expand All @@ -29,8 +36,8 @@
},
"require": {
"php": ">=7.2.0",
"martijnc/php-csp": "^1.0",
"getkirby/composer-installer": "^1.1"
"getkirby/composer-installer": "^1.1",
"paragonie/csp-builder": "^2.3"
},
"require-dev": {
"phpunit/phpunit": "^8.3",
Expand Down
Loading

0 comments on commit 23e6612

Please sign in to comment.