CodeIgniter4/system/HTTP/ContentSecurityPolicy.php
2021-07-24 19:30:51 +08:00

744 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\HTTP;
use Config\ContentSecurityPolicy as ContentSecurityPolicyConfig;
/**
* Provides tools for working with the Content-Security-Policy header
* to help defeat XSS attacks.
*
* @see http://www.w3.org/TR/CSP/
* @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/
* @see http://content-security-policy.com/
* @see https://www.owasp.org/index.php/Content_Security_Policy
*/
class ContentSecurityPolicy
{
/**
* Used for security enforcement
*
* @var array|string
*/
protected $baseURI = [];
/**
* Used for security enforcement
*
* @var array|string
*/
protected $childSrc = [];
/**
* Used for security enforcement
*
* @var array
*/
protected $connectSrc = [];
/**
* Used for security enforcement
*
* @var array|string
*/
protected $defaultSrc = [];
/**
* Used for security enforcement
*
* @var array|string
*/
protected $fontSrc = [];
/**
* Used for security enforcement
*
* @var array|string
*/
protected $formAction = [];
/**
* Used for security enforcement
*
* @var array|string
*/
protected $frameAncestors = [];
/**
* Used for security enforcement
*
* @var array|string
*/
protected $frameSrc = [];
/**
* Used for security enforcement
*
* @var array|string
*/
protected $imageSrc = [];
/**
* Used for security enforcement
*
* @var array|string
*/
protected $mediaSrc = [];
/**
* Used for security enforcement
*
* @var array|string
*/
protected $objectSrc = [];
/**
* Used for security enforcement
*
* @var array|string
*/
protected $pluginTypes = [];
/**
* Used for security enforcement
*
* @var string
*/
protected $reportURI;
/**
* Used for security enforcement
*
* @var array|string
*/
protected $sandbox = [];
/**
* Used for security enforcement
*
* @var array|string
*/
protected $scriptSrc = [];
/**
* Used for security enforcement
*
* @var array|string
*/
protected $styleSrc = [];
/**
* Used for security enforcement
*
* @var array|string
*/
protected $manifestSrc = [];
/**
* Used for security enforcement
*
* @var bool
*/
protected $upgradeInsecureRequests = false;
/**
* Used for security enforcement
*
* @var bool
*/
protected $reportOnly = false;
/**
* Used for security enforcement
*
* @var array
*/
protected $validSources = [
'self',
'none',
'unsafe-inline',
'unsafe-eval',
];
/**
* Used for security enforcement
*
* @var array
*/
protected $nonces = [];
/**
* An array of header info since we have
* to build ourself before passing to Response.
*
* @var array
*/
protected $tempHeaders = [];
/**
* An array of header info to build
* that should only be reported.
*
* @var array
*/
protected $reportOnlyHeaders = [];
/**
* Constructor.
*
* Stores our default values from the Config file.
*/
public function __construct(ContentSecurityPolicyConfig $config)
{
foreach (get_object_vars($config) as $setting => $value) {
if (property_exists($this, $setting)) {
$this->{$setting} = $value;
}
}
}
/**
* Compiles and sets the appropriate headers in the request.
*
* Should be called just prior to sending the response to the user agent.
*
* @return void
*/
public function finalize(ResponseInterface &$response)
{
$this->generateNonces($response);
$this->buildHeaders($response);
}
/**
* If TRUE, nothing will be restricted. Instead all violations will
* be reported to the reportURI for monitoring. This is useful when
* you are just starting to implement the policy, and will help
* determine what errors need to be addressed before you turn on
* all filtering.
*
* @return $this
*/
public function reportOnly(bool $value = true)
{
$this->reportOnly = $value;
return $this;
}
/**
* Adds a new base_uri value. Can be either a URI class or a simple string.
*
* base_uri restricts the URLs that can appear in a pages <base> element.
*
* @see http://www.w3.org/TR/CSP/#directive-base-uri
*
* @param array|string $uri
*
* @return $this
*/
public function addBaseURI($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'baseURI', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new valid endpoint for a form's action. Can be either
* a URI class or a simple string.
*
* child-src lists the URLs for workers and embedded frame contents.
* For example: child-src https://youtube.com would enable embedding
* videos from YouTube but not from other origins.
*
* @see http://www.w3.org/TR/CSP/#directive-child-src
*
* @param array|string $uri
*
* @return $this
*/
public function addChildSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'childSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new valid endpoint for a form's action. Can be either
* a URI class or a simple string.
*
* connect-src limits the origins to which you can connect
* (via XHR, WebSockets, and EventSource).
*
* @see http://www.w3.org/TR/CSP/#directive-connect-src
*
* @param array|string $uri
*
* @return $this
*/
public function addConnectSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'connectSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new valid endpoint for a form's action. Can be either
* a URI class or a simple string.
*
* default_src is the URI that is used for many of the settings when
* no other source has been set.
*
* @see http://www.w3.org/TR/CSP/#directive-default-src
*
* @param array|string $uri
*
* @return $this
*/
public function setDefaultSrc($uri, ?bool $explicitReporting = null)
{
$this->defaultSrc = [(string) $uri => $explicitReporting ?? $this->reportOnly];
return $this;
}
/**
* Adds a new valid endpoint for a form's action. Can be either
* a URI class or a simple string.
*
* font-src specifies the origins that can serve web fonts.
*
* @see http://www.w3.org/TR/CSP/#directive-font-src
*
* @param array|string $uri
*
* @return $this
*/
public function addFontSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'fontSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new valid endpoint for a form's action. Can be either
* a URI class or a simple string.
*
* @see http://www.w3.org/TR/CSP/#directive-form-action
*
* @param array|string $uri
*
* @return $this
*/
public function addFormAction($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'formAction', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new resource that should allow embedding the resource using
* <frame>, <iframe>, <object>, <embed>, or <applet>
*
* @see http://www.w3.org/TR/CSP/#directive-frame-ancestors
*
* @param array|string $uri
*
* @return $this
*/
public function addFrameAncestor($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'frameAncestors', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new valid endpoint for valid frame sources. Can be either
* a URI class or a simple string.
*
* @see http://www.w3.org/TR/CSP/#directive-frame-src
*
* @param array|string $uri
*
* @return $this
*/
public function addFrameSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'frameSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new valid endpoint for valid image sources. Can be either
* a URI class or a simple string.
*
* @see http://www.w3.org/TR/CSP/#directive-img-src
*
* @param array|string $uri
*
* @return $this
*/
public function addImageSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'imageSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new valid endpoint for valid video and audio. Can be either
* a URI class or a simple string.
*
* @see http://www.w3.org/TR/CSP/#directive-media-src
*
* @param array|string $uri
*
* @return $this
*/
public function addMediaSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'mediaSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new valid endpoint for manifest sources. Can be either
* a URI class or simple string.
*
* @see https://www.w3.org/TR/CSP/#directive-manifest-src
*
* @param array|string $uri
*
* @return $this
*/
public function addManifestSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'manifestSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new valid endpoint for Flash and other plugin sources. Can be either
* a URI class or a simple string.
*
* @see http://www.w3.org/TR/CSP/#directive-object-src
*
* @param array|string $uri
*
* @return $this
*/
public function addObjectSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'objectSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Limits the types of plugins that can be used. Can be either
* a URI class or a simple string.
*
* @see http://www.w3.org/TR/CSP/#directive-plugin-types
*
* @param array|string $mime One or more plugin mime types, separate by spaces
*
* @return $this
*/
public function addPluginType($mime, ?bool $explicitReporting = null)
{
$this->addOption($mime, 'pluginTypes', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Specifies a URL where a browser will send reports when a content
* security policy is violated. Can be either a URI class or a simple string.
*
* @see http://www.w3.org/TR/CSP/#directive-report-uri
*
* @return $this
*/
public function setReportURI(string $uri)
{
$this->reportURI = $uri;
return $this;
}
/**
* specifies an HTML sandbox policy that the user agent applies to
* the protected resource.
*
* @see http://www.w3.org/TR/CSP/#directive-sandbox
*
* @param array|string $flags An array of sandbox flags that can be added to the directive.
*
* @return $this
*/
public function addSandbox($flags, ?bool $explicitReporting = null)
{
$this->addOption($flags, 'sandbox', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new valid endpoint for javascript file sources. Can be either
* a URI class or a simple string.
*
* @see http://www.w3.org/TR/CSP/#directive-connect-src
*
* @param array|string $uri
*
* @return $this
*/
public function addScriptSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'scriptSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new valid endpoint for CSS file sources. Can be either
* a URI class or a simple string.
*
* @see http://www.w3.org/TR/CSP/#directive-connect-src
*
* @param array|string $uri
*
* @return $this
*/
public function addStyleSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'styleSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Sets whether the user agents should rewrite URL schemes, changing
* HTTP to HTTPS.
*
* @return $this
*/
public function upgradeInsecureRequests(bool $value = true)
{
$this->upgradeInsecureRequests = $value;
return $this;
}
/**
* DRY method to add an string or array to a class property.
*
* @param array|string $options
*
* @return void
*/
protected function addOption($options, string $target, ?bool $explicitReporting = null)
{
// Ensure we have an array to work with...
if (is_string($this->{$target})) {
$this->{$target} = [$this->{$target}];
}
if (is_array($options)) {
foreach ($options as $opt) {
$this->{$target}[$opt] = $explicitReporting ?? $this->reportOnly;
}
} else {
$this->{$target}[$options] = $explicitReporting ?? $this->reportOnly;
}
}
/**
* Scans the body of the request message and replaces any nonce
* placeholders with actual nonces, that we'll then add to our
* headers.
*
* @return void
*/
protected function generateNonces(ResponseInterface &$response)
{
$body = $response->getBody();
if (empty($body)) {
return;
}
if (! is_array($this->styleSrc)) {
$this->styleSrc = [$this->styleSrc];
}
if (! is_array($this->scriptSrc)) {
$this->scriptSrc = [$this->scriptSrc];
}
// Replace style placeholders with nonces
$body = preg_replace_callback('/{csp-style-nonce}/', function () {
$nonce = bin2hex(random_bytes(12));
$this->styleSrc[] = 'nonce-' . $nonce;
return "nonce=\"{$nonce}\"";
}, $body);
// Replace script placeholders with nonces
$body = preg_replace_callback('/{csp-script-nonce}/', function () {
$nonce = bin2hex(random_bytes(12));
$this->scriptSrc[] = 'nonce-' . $nonce;
return "nonce=\"{$nonce}\"";
}, $body);
$response->setBody($body);
}
/**
* Based on the current state of the elements, will add the appropriate
* Content-Security-Policy and Content-Security-Policy-Report-Only headers
* with their values to the response object.
*
* @return void
*/
protected function buildHeaders(ResponseInterface &$response)
{
/**
* Ensure both headers are available and arrays...
*
* @var Response $response
*/
$response->setHeader('Content-Security-Policy', []);
$response->setHeader('Content-Security-Policy-Report-Only', []);
$directives = [
'base-uri' => 'baseURI',
'child-src' => 'childSrc',
'connect-src' => 'connectSrc',
'default-src' => 'defaultSrc',
'font-src' => 'fontSrc',
'form-action' => 'formAction',
'frame-ancestors' => 'frameAncestors',
'frame-src' => 'frameSrc',
'img-src' => 'imageSrc',
'media-src' => 'mediaSrc',
'object-src' => 'objectSrc',
'plugin-types' => 'pluginTypes',
'script-src' => 'scriptSrc',
'style-src' => 'styleSrc',
'manifest-src' => 'manifestSrc',
'sandbox' => 'sandbox',
'report-uri' => 'reportURI',
];
// inject default base & default URIs if needed
if (empty($this->baseURI)) {
$this->baseURI = 'self';
}
if (empty($this->defaultSrc)) {
$this->defaultSrc = 'self';
}
foreach ($directives as $name => $property) {
if (! empty($this->{$property})) {
$this->addToHeader($name, $this->{$property});
}
}
// Compile our own header strings here since if we just
// append it to the response, it will be joined with
// commas, not semi-colons as we need.
if (! empty($this->tempHeaders)) {
$header = '';
foreach ($this->tempHeaders as $name => $value) {
$header .= " {$name} {$value};";
}
// add token only if needed
if ($this->upgradeInsecureRequests) {
$header .= ' upgrade-insecure-requests;';
}
$response->appendHeader('Content-Security-Policy', $header);
}
if (! empty($this->reportOnlyHeaders)) {
$header = '';
foreach ($this->reportOnlyHeaders as $name => $value) {
$header .= " {$name} {$value};";
}
$response->appendHeader('Content-Security-Policy-Report-Only', $header);
}
$this->tempHeaders = [];
$this->reportOnlyHeaders = [];
}
/**
* Adds a directive and it's options to the appropriate header. The $values
* array might have options that are geared toward either the regular or the
* reportOnly header, since it's viable to have both simultaneously.
*
* @param array|string|null $values
*
* @return void
*/
protected function addToHeader(string $name, $values = null)
{
if (is_string($values)) {
$values = [$values => 0];
}
$sources = [];
$reportSources = [];
foreach ($values as $value => $reportOnly) {
if (is_numeric($value) && is_string($reportOnly) && ! empty($reportOnly)) {
$value = $reportOnly;
$reportOnly = 0;
}
if ($reportOnly === true) {
$reportSources[] = in_array($value, $this->validSources, true) ? "'{$value}'" : $value;
} elseif (strpos($value, 'nonce-') === 0) {
$sources[] = "'{$value}'";
} else {
$sources[] = in_array($value, $this->validSources, true) ? "'{$value}'" : $value;
}
}
if (! empty($sources)) {
$this->tempHeaders[$name] = implode(' ', $sources);
}
if (! empty($reportSources)) {
$this->reportOnlyHeaders[$name] = implode(' ', $reportSources);
}
}
}