icingaweb2/library/Icinga/Util/Csp.php

108 lines
2.8 KiB
PHP
Raw Normal View History

2023-06-27 11:32:51 +02:00
<?php
/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
namespace Icinga\Util;
use Icinga\Web\Response;
use Icinga\Web\Window;
use RuntimeException;
use function ipl\Stdlib\get_php_type;
/**
* Helper to enable strict content security policy (CSP)
*
* {@see static::addHeader()} adds a strict Content-Security-Policy header with a nonce to still support dynamic CSS
* securely.
* Note that {@see static::createNonce()} must be called first.
* Use {@see static::getStyleNonce()} to access the nonce for dynamic CSS.
*
* A nonce is not created for dynamic JS,
* and it is questionable whether this will ever be supported.
*/
class Csp
{
/** @var static */
protected static $instance;
/** @var ?string */
protected $styleNonce;
/** Singleton */
private function __construct()
{
}
/**
* Add Content-Security-Policy header with a nonce for dynamic CSS
*
* Note that {@see static::createNonce()} must be called beforehand.
*
* @param Response $response
*
* @throws RuntimeException If no nonce set for CSS
*/
public static function addHeader(Response $response): void
{
$csp = static::getInstance();
if (empty($csp->styleNonce)) {
throw new RuntimeException('No nonce set for CSS');
}
$response->setHeader('Content-Security-Policy', "style-src 'self' 'nonce-$csp->styleNonce';", true);
}
/**
* Set/recreate nonce for dynamic CSS
*
* Should always be called upon initial page loads or page reloads,
* as it sets/recreates a nonce for CSS and writes it to a window-aware session.
*/
public static function createNonce(): void
{
$csp = static::getInstance();
$csp->styleNonce = base64_encode(random_bytes(16));
Window::getInstance()->getSessionNamespace('csp')->set('style_nonce', $csp->styleNonce);
}
/**
* Get nonce for dynamic CSS
*
* @return ?string
*/
public static function getStyleNonce(): ?string
{
return static::getInstance()->styleNonce;
}
/**
* Get the CSP instance
*
* @return self
*/
protected static function getInstance(): self
{
if (static::$instance === null) {
$csp = new static();
$nonce = Window::getInstance()->getSessionNamespace('csp')->get('style_nonce');
if ($nonce !== null && ! is_string($nonce)) {
throw new RuntimeException(
sprintf(
'Nonce value is expected to be string, got %s instead',
get_php_type($nonce)
)
);
}
$csp->styleNonce = $nonce;
static::$instance = $csp;
}
return static::$instance;
}
}