diff --git a/application/Config/ContentSecurityPolicy.php b/application/Config/ContentSecurityPolicy.php index d0147f081995..0e6955a526b1 100644 --- a/application/Config/ContentSecurityPolicy.php +++ b/application/Config/ContentSecurityPolicy.php @@ -9,43 +9,40 @@ * choose to use it. The values here will be read in and set as defaults * for the site. If needed, they can be overridden on a page-by-page basis. * + * Suggested reference for explanations: + * https://www.html5rocks.com/en/tutorials/security/content-security-policy/ + * * @package Config */ class ContentSecurityPolicy extends BaseConfig { - public $reportOnly = false; - - public $defaultSrc = 'none'; - - public $scriptSrc = 'self'; - - public $styleSrc = 'self'; - - public $imageSrc = 'self'; - - public $baseURI = 'none'; - - public $childSrc = null; - - public $connectSrc = 'self'; - - public $fontSrc = null; - - public $formAction = null; - + // broadbrush CSP management + + public $reportOnly = false; // default CSP report context + public $reportURI = null; // URL to send violation reports to + public $upgradeInsecureRequests = false; // toggle for forcing https + + // sources allowed; string or array of strings + // Note: once you set a policy to 'none', it cannot be further restricted + + public $defaultSrc = null; // will default to self if not over-ridden + public $scriptSrc = 'self'; + public $styleSrc = 'self'; + public $imageSrc = 'self'; + public $baseURI = null; // will default to self if not over-ridden + public $childSrc = 'self'; + public $connectSrc = 'self'; + public $fontSrc = null; + public $formAction = 'self'; public $frameAncestors = null; + public $mediaSrc = null; + public $objectSrc = 'self'; + public $manifestSrc = null; - public $mediaSrc = null; - - public $objectSrc = null; - - public $manifestSrc = null; - + // mime types allowed; string or array of strings public $pluginTypes = null; - public $reportURI = null; - - public $sandbox = false; + // list of actions allowed; string or array of strings + public $sandbox = null; - public $upgradeInsecureRequests = false; } diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php index 20f6c0facc8b..cf94f2f727f9 100644 --- a/system/HTTP/ContentSecurityPolicy.php +++ b/system/HTTP/ContentSecurityPolicy.php @@ -1,4 +1,5 @@ -buildHeaders($response); } - //-------------------------------------------------------------------- - //-------------------------------------------------------------------- - // Setters //-------------------------------------------------------------------- /** @@ -274,20 +272,20 @@ public function reportOnly(bool $value = true) //-------------------------------------------------------------------- /** - * Sets the base_uri value. Can be either a URI class or a simple string. + * 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 element. * * @see http://www.w3.org/TR/CSP/#directive-base-uri * - * @param string $uri - * @param boolean $reportOnly + * @param string|array $uri + * @param boolean|null $explicitReporting * * @return $this */ - public function setBaseURI($uri, bool $reportOnly) + public function addBaseURI($uri, ?bool $explicitReporting = null) { - $this->baseURI = [(string) $uri => $reportOnly]; + $this->addOption($uri, 'baseURI', $explicitReporting ?? $this->reportOnly); return $this; } @@ -304,14 +302,14 @@ public function setBaseURI($uri, bool $reportOnly) * * @see http://www.w3.org/TR/CSP/#directive-child-src * - * @param $uri - * @param boolean $reportOnly + * @param string|array $uri + * @param boolean|null $explicitReporting * * @return $this */ - public function addChildSrc($uri, bool $reportOnly = false) + public function addChildSrc($uri, ?bool $explicitReporting = null) { - $this->addOption($uri, 'childSrc', $reportOnly); + $this->addOption($uri, 'childSrc', $explicitReporting ?? $this->reportOnly); return $this; } @@ -327,14 +325,14 @@ public function addChildSrc($uri, bool $reportOnly = false) * * @see http://www.w3.org/TR/CSP/#directive-connect-src * - * @param $uri - * @param boolean $reportOnly + * @param string|array $uri + * @param boolean|null $explicitReporting * * @return $this */ - public function addConnectSrc($uri, bool $reportOnly = false) + public function addConnectSrc($uri, ?bool $explicitReporting = null) { - $this->addOption($uri, 'connectSrc', $reportOnly); + $this->addOption($uri, 'connectSrc', $explicitReporting ?? $this->reportOnly); return $this; } @@ -350,14 +348,14 @@ public function addConnectSrc($uri, bool $reportOnly = false) * * @see http://www.w3.org/TR/CSP/#directive-default-src * - * @param $uri - * @param boolean $reportOnly + * @param string|array $uri + * @param boolean|null $explicitReporting * * @return $this */ - public function setDefaultSrc($uri, bool $reportOnly = false) + public function setDefaultSrc($uri, ?bool $explicitReporting = null) { - $this->defaultSrc = [(string) $uri => $reportOnly]; + $this->defaultSrc = [(string) $uri => $explicitReporting ?? $this->reportOnly]; return $this; } @@ -372,14 +370,14 @@ public function setDefaultSrc($uri, bool $reportOnly = false) * * @see http://www.w3.org/TR/CSP/#directive-font-src * - * @param $uri - * @param boolean $reportOnly + * @param string|array $uri + * @param boolean|null $explicitReporting * * @return $this */ - public function addFontSrc($uri, bool $reportOnly = false) + public function addFontSrc($uri, ?bool $explicitReporting = null) { - $this->addOption($uri, 'fontSrc', $reportOnly); + $this->addOption($uri, 'fontSrc', $explicitReporting ?? $this->reportOnly); return $this; } @@ -392,14 +390,14 @@ public function addFontSrc($uri, bool $reportOnly = false) * * @see http://www.w3.org/TR/CSP/#directive-form-action * - * @param $uri - * @param boolean $reportOnly + * @param string|array $uri + * @param boolean|null $explicitReporting * * @return $this */ - public function addFormAction($uri, bool $reportOnly = false) + public function addFormAction($uri, ?bool $explicitReporting = null) { - $this->addOption($uri, 'formAction', $reportOnly); + $this->addOption($uri, 'formAction', $explicitReporting ?? $this->reportOnly); return $this; } @@ -412,14 +410,14 @@ public function addFormAction($uri, bool $reportOnly = false) * * @see http://www.w3.org/TR/CSP/#directive-frame-ancestors * - * @param $uri - * @param boolean $reportOnly + * @param string|array $uri + * @param boolean|null $explicitReporting * * @return $this */ - public function addFrameAncestor($uri, bool $reportOnly = false) + public function addFrameAncestor($uri, ?bool $explicitReporting = null) { - $this->addOption($uri, 'frameAncestors', $reportOnly); + $this->addOption($uri, 'frameAncestors', $explicitReporting ?? $this->reportOnly); return $this; } @@ -432,14 +430,14 @@ public function addFrameAncestor($uri, bool $reportOnly = false) * * @see http://www.w3.org/TR/CSP/#directive-img-src * - * @param $uri - * @param boolean $reportOnly + * @param string|array $uri + * @param boolean|null $explicitReporting * * @return $this */ - public function addImageSrc($uri, bool $reportOnly = false) + public function addImageSrc($uri, ?bool $explicitReporting = null) { - $this->addOption($uri, 'imageSrc', $reportOnly); + $this->addOption($uri, 'imageSrc', $explicitReporting ?? $this->reportOnly); return $this; } @@ -452,14 +450,14 @@ public function addImageSrc($uri, bool $reportOnly = false) * * @see http://www.w3.org/TR/CSP/#directive-media-src * - * @param $uri - * @param boolean $reportOnly + * @param string|array $uri + * @param boolean|null $explicitReporting * * @return $this */ - public function addMediaSrc($uri, bool $reportOnly = false) + public function addMediaSrc($uri, ?bool $explicitReporting = null) { - $this->addOption($uri, 'mediaSrc', $reportOnly); + $this->addOption($uri, 'mediaSrc', $explicitReporting ?? $this->reportOnly); return $this; } @@ -472,14 +470,14 @@ public function addMediaSrc($uri, bool $reportOnly = false) * * @see https://www.w3.org/TR/CSP/#directive-manifest-src * - * @param $uri - * @param boolean $reportOnly + * @param string|array $uri + * @param boolean|null $explicitReporting * * @return $this */ - public function addManifestSrc($uri, bool $reportOnly = false) + public function addManifestSrc($uri, ?bool $explicitReporting = null) { - $this->addOption($uri, 'manifestSrc', $reportOnly); + $this->addOption($uri, 'manifestSrc', $explicitReporting ?? $this->reportOnly); return $this; } @@ -492,14 +490,14 @@ public function addManifestSrc($uri, bool $reportOnly = false) * * @see http://www.w3.org/TR/CSP/#directive-object-src * - * @param $uri - * @param boolean $reportOnly + * @param string|array $uri + * @param boolean|null $explicitReporting * * @return $this */ - public function addObjectSrc($uri, bool $reportOnly = false) + public function addObjectSrc($uri, ?bool $explicitReporting = null) { - $this->addOption($uri, 'objectSrc', $reportOnly); + $this->addOption($uri, 'objectSrc', $explicitReporting ?? $this->reportOnly); return $this; } @@ -512,14 +510,14 @@ public function addObjectSrc($uri, bool $reportOnly = false) * * @see http://www.w3.org/TR/CSP/#directive-plugin-types * - * @param string $mime One or more plugin mime types, separate by spaces - * @param boolean $reportOnly + * @param string|array $mime One or more plugin mime types, separate by spaces + * @param boolean|null $explicitReporting * * @return $this */ - public function addPluginType($mime, bool $reportOnly = false) + public function addPluginType($mime, ?bool $explicitReporting = null) { - $this->addOption($mime, 'pluginTypes', $reportOnly); + $this->addOption($mime, 'pluginTypes', $explicitReporting ?? $this->reportOnly); return $this; } @@ -532,7 +530,7 @@ public function addPluginType($mime, bool $reportOnly = false) * * @see http://www.w3.org/TR/CSP/#directive-report-uri * - * @param $uri + * @param string $uri * * @return $this */ @@ -551,22 +549,14 @@ public function setReportURI($uri) * * @see http://www.w3.org/TR/CSP/#directive-sandbox * - * @param boolean $value - * @param array $flags An array of sandbox flags that can be added to the directive. + * @param string|array $flags An array of sandbox flags that can be added to the directive. + * @param boolean|null $explicitReporting * * @return $this */ - public function setSandbox(bool $value = true, array $flags = null) + public function addSandbox($flags, ?bool $explicitReporting = null) { - if (empty($this->sandbox) && empty($flags)) - { - $this->sandbox = $value; - } - else - { - $this->sandbox = $flags; - } - + $this->addOption($flags, 'sandbox', $explicitReporting ?? $this->reportOnly); return $this; } @@ -578,14 +568,14 @@ public function setSandbox(bool $value = true, array $flags = null) * * @see http://www.w3.org/TR/CSP/#directive-connect-src * - * @param $uri - * @param boolean $reportOnly + * @param string|array $uri + * @param boolean|null $explicitReporting * * @return $this */ - public function addScriptSrc($uri, bool $reportOnly = false) + public function addScriptSrc($uri, ?bool $explicitReporting = null) { - $this->addOption($uri, 'scriptSrc', $reportOnly); + $this->addOption($uri, 'scriptSrc', $explicitReporting ?? $this->reportOnly); return $this; } @@ -598,14 +588,14 @@ public function addScriptSrc($uri, bool $reportOnly = false) * * @see http://www.w3.org/TR/CSP/#directive-connect-src * - * @param $uri - * @param boolean $reportOnly + * @param string|array $uri + * @param boolean|null $explicitReporting * * @return $this */ - public function addStyleSrc($uri, bool $reportOnly = false) + public function addStyleSrc($uri, ?bool $explicitReporting = null) { - $this->addOption($uri, 'styleSrc', $reportOnly); + $this->addOption($uri, 'styleSrc', $explicitReporting ?? $this->reportOnly); return $this; } @@ -616,7 +606,7 @@ public function addStyleSrc($uri, bool $reportOnly = false) * Sets whether the user agents should rewrite URL schemes, changing * HTTP to HTTPS. * - * @param boolean|true $value + * @param boolean $value * * @return $this */ @@ -627,7 +617,6 @@ public function upgradeInsecureRequests(bool $value = true) return $this; } - //-------------------------------------------------------------------- //-------------------------------------------------------------------- // Utility //-------------------------------------------------------------------- @@ -635,11 +624,11 @@ public function upgradeInsecureRequests(bool $value = true) /** * DRY method to add an string or array to a class property. * - * @param $options - * @param string $target - * @param boolean $reportOnly If TRUE, this item will be reported, not restricted + * @param string|array $options + * @param string $target + * @param boolean|null $explicitReporting */ - protected function addOption($options, string $target, bool $reportOnly = false) + protected function addOption($options, string $target, ?bool $explicitReporting = null) { // Ensure we have an array to work with... if (is_string($this->{$target})) @@ -649,18 +638,14 @@ protected function addOption($options, string $target, bool $reportOnly = false) if (is_array($options)) { - $newOptions = []; foreach ($options as $opt) { - $newOptions[] = [$opt => $reportOnly]; + $this->{$target}[$opt] = $explicitReporting ?? $this->reportOnly; } - - $this->{$target} = array_merge($this->{$target}, $newOptions); - unset($newOptions); } else { - $this->{$target}[$options] = $reportOnly; + $this->{$target}[$options] = $explicitReporting ?? $this->reportOnly; } } @@ -750,6 +735,16 @@ protected function buildHeaders(ResponseInterface &$response) '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) { // base_uri @@ -769,6 +764,12 @@ protected function buildHeaders(ResponseInterface &$response) { $header .= " {$name} {$value};"; } + // add token only if needed + if ($this->upgradeInsecureRequests) + { + $header .= ' upgrade-insecure-requests;'; + } + $response->appendHeader('Content-Security-Policy', $header); } @@ -800,8 +801,6 @@ protected function addToHeader(string $name, $values = null) { if (empty($values)) { - // It's possible that directives like 'sandbox' will not - // have any values passed in, so add them to the main policy. $this->tempHeaders[$name] = null; return; } diff --git a/system/Test/CIUnitTestCase.php b/system/Test/CIUnitTestCase.php index e2b6c8ac4635..7ca469c95853 100644 --- a/system/Test/CIUnitTestCase.php +++ b/system/Test/CIUnitTestCase.php @@ -284,4 +284,32 @@ protected function adjustPaths(Paths $paths) return $paths; } + //-------------------------------------------------------------------- + /** + * Return first matching emitted header. + * + * @param string $header Identifier of the header of interest + * @param bool $ignoreCase + * + * @return string|null The value of the header found, null if not found + */ + // + protected function getHeaderEmitted(string $header, bool $ignoreCase = false): ?string + { + $found = false; + + foreach (xdebug_get_headers() as $emitted) + { + $found = $ignoreCase ? + (stripos($emitted, $header) === 0) : + (strpos($emitted, $header) === 0); + if ($found) + { + return $emitted; + } + } + + return null; + } + } diff --git a/tests/system/HTTP/ContentSecurityPolicyTest.php b/tests/system/HTTP/ContentSecurityPolicyTest.php new file mode 100644 index 000000000000..cd861ad008fb --- /dev/null +++ b/tests/system/HTTP/ContentSecurityPolicyTest.php @@ -0,0 +1,411 @@ +CSPEnabled = true; + $this->response = new Response($config); + $this->response->pretend(false); + $this->csp = $this->response->CSP; + } + + protected function work() + { + $body = 'Hello'; + $this->response->setBody($body); + $this->response->setCookie('foo', 'bar'); + + ob_start(); + $this->response->send(); + $buffer = ob_clean(); + if (ob_get_level() > 0) + { + ob_end_clean(); + } + return $buffer; + } + + //-------------------------------------------------------------------- + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testExistence() + { + $this->prepare(); + $result = $this->work(); + + $this->assertHeaderEmitted('Content-Security-Policy:'); + } + + //-------------------------------------------------------------------- + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testReportOnly() + { + $this->prepare(); + $this->csp->reportOnly(false); + $result = $this->work(); + + $this->assertHeaderEmitted('Content-Security-Policy:'); + } + + //-------------------------------------------------------------------- + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testDefaults() + { + $this->prepare(); + + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains("base-uri 'self';", $result); + $this->assertContains("connect-src 'self';", $result); + $this->assertContains("default-src 'self';", $result); + $this->assertContains("img-src 'self';", $result); + $this->assertContains("script-src 'self';", $result); + $this->assertContains("style-src 'self';", $result); + } + + //-------------------------------------------------------------------- + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testChildSrc() + { + $this->prepare(); + $this->csp->addChildSrc('evil.com', true); + $this->csp->addChildSrc('good.com', false); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertContains('child-src evil.com;', $result); + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains("child-src 'self' good.com;", $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testConnectSrc() + { + $this->prepare(); + $this->csp->reportOnly(true); + $this->csp->addConnectSrc('iffy.com'); + $this->csp->addConnectSrc('maybe.com'); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertContains('connect-src iffy.com maybe.com;', $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testFontSrc() + { + $this->prepare(); + $this->csp->reportOnly(true); + $this->csp->addFontSrc('iffy.com'); + $this->csp->addFontSrc('fontsrus.com', false); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertContains('font-src iffy.com;', $result); + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains('font-src fontsrus.com;', $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testFormAction() + { + $this->prepare(); + $this->csp->reportOnly(true); + $this->csp->addFormAction('surveysrus.com'); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertContains('form-action surveysrus.com;', $result); + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains("form-action 'self';", $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testFrameAncestor() + { + $this->prepare(); + $this->csp->addFrameAncestor('self'); + $this->csp->addFrameAncestor('them.com', true); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertContains('frame-ancestors them.com;', $result); + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains("frame-ancestors 'self';", $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testImageSrc() + { + $this->prepare(); + $this->csp->addImageSrc('cdn.cloudy.com'); + $this->csp->addImageSrc('them.com', true); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertContains('img-src them.com;', $result); + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains("img-src 'self' cdn.cloudy.com;", $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testMediaSrc() + { + $this->prepare(); + $this->csp->addMediaSrc('self'); + $this->csp->addMediaSrc('them.com', true); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertContains('media-src them.com;', $result); + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains("media-src 'self';", $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testManifestSrc() + { + $this->prepare(); + $this->csp->addManifestSrc('cdn.cloudy.com'); + $this->csp->addManifestSrc('them.com', true); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertContains('manifest-src them.com;', $result); + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains('manifest-src cdn.cloudy.com;', $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testPluginType() + { + $this->prepare(); + $this->csp->addPluginType('self'); + $this->csp->addPluginType('application/x-shockwave-flash', true); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertContains('plugin-types application/x-shockwave-flash;', $result); + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains("plugin-types 'self';", $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testPluginArray() + { + $this->prepare(); + $this->csp->addPluginType('application/x-shockwave-flash'); + $this->csp->addPluginType('application/wacky-hacky'); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains('plugin-types application/x-shockwave-flash application/wacky-hacky;', $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testObjectSrc() + { + $this->prepare(); + $this->csp->addObjectSrc('cdn.cloudy.com'); + $this->csp->addObjectSrc('them.com', true); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertContains('object-src them.com;', $result); + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains("object-src 'self' cdn.cloudy.com;", $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testScriptSrc() + { + $this->prepare(); + $this->csp->addScriptSrc('cdn.cloudy.com'); + $this->csp->addScriptSrc('them.com', true); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertContains('script-src them.com;', $result); + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains("script-src 'self' cdn.cloudy.com;", $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testStyleSrc() + { + $this->prepare(); + $this->csp->addStyleSrc('cdn.cloudy.com'); + $this->csp->addStyleSrc('them.com', true); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertContains('style-src them.com;', $result); + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains("style-src 'self' cdn.cloudy.com;", $result); + } + + //-------------------------------------------------------------------- + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testBaseURIDefault() + { + $this->prepare(); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains("base-uri 'self';", $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testBaseURI() + { + $this->prepare(); + $this->csp->addBaseURI('example.com'); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains('base-uri example.com;', $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testBaseURIRich() + { + $this->prepare(); + $this->csp->addBaseURI(['self', 'example.com']); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains("base-uri 'self' example.com;", $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testDefaultSrc() + { + $this->prepare(); + $this->csp->reportOnly(false); + $this->csp->setDefaultSrc('maybe.com'); + $this->csp->setDefaultSrc('iffy.com'); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains('default-src iffy.com;', $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testReportURI() + { + $this->prepare(); + $this->csp->reportOnly(false); + $this->csp->setReportURI('http://example.com/csptracker'); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains('report-uri http://example.com/csptracker;', $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSandboxFlags() + { + $this->prepare(); + $this->csp->reportOnly(false); + $this->csp->addSandbox(['allow-popups', 'allow-top-navigation']); + // $this->csp->addSandbox('allow-popups'); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains('sandbox allow-popups allow-top-navigation;', $result); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testUpgradeInsecureRequests() + { + $this->prepare(); + $this->csp->upgradeInsecureRequests(); + $result = $this->work(); + + $result = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertContains('upgrade-insecure-requests;', $result); + } + +} diff --git a/user_guide_src/source/outgoing/response.rst b/user_guide_src/source/outgoing/response.rst index 9c33996d4cee..cd560c8ec4ee 100644 --- a/user_guide_src/source/outgoing/response.rst +++ b/user_guide_src/source/outgoing/response.rst @@ -163,32 +163,64 @@ When enabled, the response object will contain an instance of ``CodeIgniter\HTTP values set in **application/Config/ContentSecurityPolicy.php** are applied to that instance and, if no changes are needed during runtime, then the correctly formatted header is sent and you're all done. +With CSP enabled, two header lines are added to the HTTP response: a Content-Security-Policy header, with +policies identifying content types or origins that are explicitly allowed for different +contexts, and a Content-Security-Policy-Report-Only header, which identifies content types +or origins that will be allowed but which will also be reported to the destination +of your choice. + +Our implementation provides for a default treatment, changeable through the ``reportOnly()`` method. +When an additional entry is added to a CSP directive, as shown below, it will be added +to the CSP header appropriate for blocking or preventing. That can be over-ridden on a per +call basis, by providing an optional second parameter to the adding method call. + Runtime Configuration --------------------- If your application needs to make changes at run-time, you can access the instance at ``$response->CSP``. The -class holds a number of methods that map pretty clearly to the appropriate header value that you need to set:: - - $reportOnly = true; - - $response->CSP->reportOnly($reportOnly); - $response->CSP->setBaseURI('example.com', true); - $response->CSP->setDefaultSrc('cdn.example.com', $reportOnly); +class holds a number of methods that map pretty clearly to the appropriate header value that you need to set. +Examples are shown below, with different combinations of parameters, though all accept either a directive +name or anarray of them.:: + + // specify the default directive treatment + $response->CSP->reportOnly(false); + + // specify the origin to use if none provided for a directive + $response->CSP->setDefaultSrc('cdn.example.com'); + // specify the URL that "report-only" reports get sent to $response->CSP->setReportURI('http://example.com/csp/reports'); - $response->CSP->setSandbox(true, ['allow-forms', 'allow-scripts']); + // specify that HTTP requests be upgraded to HTTPS $response->CSP->upgradeInsecureRequests(true); - $response->CSP->addChildSrc('https://youtube.com', $reportOnly); - $response->CSP->addConnectSrc('https://*.facebook.com', $reportOnly); - $response->CSP->addFontSrc('fonts.example.com', $reportOnly); - $response->CSP->addFormAction('self', $reportOnly); - $response->CSP->addFrameAncestor('none', $reportOnly); - $response->CSP->addImageSrc('cdn.example.com', $reportOnly); - $response->CSP->addMediaSrc('cdn.example.com', $reportOnly); - $response->CSP->addManifestSrc('cdn.example.com', $reportOnly); - $response->CSP->addObjectSrc('cdn.example.com', $reportOnly); - $response->CSP->addPluginType('application/pdf', $reportOnly); - $response->CSP->addScriptSrc('scripts.example.com', $reportOnly); - $response->CSP->addStyleSrc('css.example.com', $reportOnly); + + // add types or origins to CSP directives + // assuming that the default treatment is to block rather than just report + $response->CSP->addBaseURI('example.com', true); // report only + $response->CSP->addChildSrc('https://youtube.com'); // blocked + $response->CSP->addConnectSrc('https://*.facebook.com', false); // blocked + $response->CSP->addFontSrc('fonts.example.com'); + $response->CSP->addFormAction('self'); + $response->CSP->addFrameAncestor('none', true); // report this one + $response->CSP->addImageSrc('cdn.example.com'); + $response->CSP->addMediaSrc('cdn.example.com'); + $response->CSP->addManifestSrc('cdn.example.com'); + $response->CSP->addObjectSrc('cdn.example.com', false); // reject from here + $response->CSP->addPluginType('application/pdf', false); // reject this media type + $response->CSP->addScriptSrc('scripts.example.com', true); // allow but report requests from here + $response->CSP->addStyleSrc('css.example.com'); + $response->CSP->addSandbox(['allow-forms', 'allow-scripts']); + + +The first parameter to each of the "add" methods is an appropriate string value, +or an array of them. + +The ``reportOnly`` method allows you to specify the default reporting treatment +for subsequent sources, unless over-ridden. For instance, you could specify +that youtube.com was allowed, and then provide several allowed but reported sources:: + + $response->addChildSrc('https://youtube.com'); // allowed + $response->reportOnly(true); + $response->addChildSrc('https://metube.com'); // allowed but reported + $response->addChildSrc('https://ourtube.com',false); // allowed Inline Content --------------