mirror of
https://github.com/codeigniter4/CodeIgniter4.git
synced 2025-02-20 11:44:28 +08:00
744 lines
19 KiB
PHP
744 lines
19 KiB
PHP
<?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 page’s <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);
|
||
}
|
||
}
|
||
}
|