diff --git a/app/Config/ContentSecurityPolicy.php b/app/Config/ContentSecurityPolicy.php index f64a9af22b0a..01cb8ec8c918 100644 --- a/app/Config/ContentSecurityPolicy.php +++ b/app/Config/ContentSecurityPolicy.php @@ -199,6 +199,16 @@ class ContentSecurityPolicy extends BaseConfig */ public $sandbox; + /** + * Enable nonce to style tags? + */ + public bool $enableStyleNonce = true; + + /** + * Enable nonce to script tags? + */ + public bool $enableScriptNonce = true; + /** * Nonce placeholder for style tags. */ diff --git a/system/Common.php b/system/Common.php index 04115993ef87..9da60952934b 100644 --- a/system/Common.php +++ b/system/Common.php @@ -335,7 +335,7 @@ function csp_style_nonce(): string { $csp = service('csp'); - if (! $csp->enabled()) { + if (! $csp->styleNonceEnabled()) { return ''; } @@ -351,7 +351,7 @@ function csp_script_nonce(): string { $csp = service('csp'); - if (! $csp->enabled()) { + if (! $csp->scriptNonceEnabled()) { return ''; } diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php index c94fed4c8e73..5006ec81dffd 100644 --- a/system/HTTP/ContentSecurityPolicy.php +++ b/system/HTTP/ContentSecurityPolicy.php @@ -298,6 +298,20 @@ class ContentSecurityPolicy */ protected $scriptNonce; + /** + * Whether to enable nonce to style-src and style-src-elem directives or not. + * + * @var bool + */ + protected $enableStyleNonce = true; + + /** + * Whether to enable nonce to script-src and script-src-elem directives or not. + * + * @var bool + */ + protected $enableScriptNonce = true; + /** * Nonce placeholder for style tags. * @@ -392,11 +406,33 @@ public function enabled(): bool return $this->CSPEnabled; } + /** + * Whether adding nonce in style-* directives is enabled or not. + */ + public function styleNonceEnabled(): bool + { + return $this->enabled() && $this->enableStyleNonce; + } + + /** + * Whether adding nonce in script-* directives is enabled or not. + */ + public function scriptNonceEnabled(): bool + { + return $this->enabled() && $this->enableScriptNonce; + } + /** * Get the nonce for the style tag. */ public function getStyleNonce(): string { + if (! $this->enableStyleNonce) { + $this->styleNonce = null; + + return ''; + } + if ($this->styleNonce === null) { $this->styleNonce = base64_encode(random_bytes(12)); $this->addStyleSrc('nonce-' . $this->styleNonce); @@ -414,6 +450,12 @@ public function getStyleNonce(): string */ public function getScriptNonce(): string { + if (! $this->enableScriptNonce) { + $this->scriptNonce = null; + + return ''; + } + if ($this->scriptNonce === null) { $this->scriptNonce = base64_encode(random_bytes(12)); $this->addScriptSrc('nonce-' . $this->scriptNonce); @@ -868,6 +910,30 @@ public function addReportingEndpoints(array $endpoint): static return $this; } + /** + * Enables or disables adding nonces to style-src and style-src-elem directives. + * + * @return $this + */ + public function setEnableStyleNonce(bool $value = true): static + { + $this->enableStyleNonce = $value; + + return $this; + } + + /** + * Enables or disables adding nonces to script-src and script-src-elem directives. + * + * @return $this + */ + public function setEnableScriptNonce(bool $value = true): static + { + $this->enableScriptNonce = $value; + + return $this; + } + /** * DRY method to add an string or array to a class property. * @@ -919,8 +985,21 @@ protected function generateNonces(ResponseInterface $response) return ''; } - $nonce = $match[0] === $this->styleNonceTag ? $this->getStyleNonce() : $this->getScriptNonce(); - $attr = 'nonce="' . $nonce . '"'; + if ($match[0] === $this->styleNonceTag) { + if (! $this->enableStyleNonce) { + return ''; + } + + $nonce = $this->getStyleNonce(); + } else { + if (! $this->enableScriptNonce) { + return ''; + } + + $nonce = $this->getScriptNonce(); + } + + $attr = 'nonce="' . $nonce . '"'; return $jsonEscape ? str_replace('"', '\\"', $attr) : $attr; }, $body); diff --git a/tests/system/HTTP/ContentSecurityPolicyTest.php b/tests/system/HTTP/ContentSecurityPolicyTest.php index e4a5cfc45ac7..de9670fdf749 100644 --- a/tests/system/HTTP/ContentSecurityPolicyTest.php +++ b/tests/system/HTTP/ContentSecurityPolicyTest.php @@ -731,6 +731,26 @@ public function testBodyScriptNonce(): void $this->assertStringContainsString('nonce-', $header); } + public function testDisabledScriptNonce(): void + { + $this->csp->clearDirective('script-src'); + + $this->csp->setEnableScriptNonce(false); + $this->csp->addScriptSrc('self'); + $this->csp->addScriptSrc('cdn.cloudy.com'); + + $this->assertTrue($this->work('')); + + $header = $this->response->getHeaderLine('Content-Security-Policy'); + $body = $this->response->getBody(); + + $this->assertIsString($body); + $this->assertStringNotContainsString('nonce=', $body); + + $this->assertStringContainsString("script-src 'self' cdn.cloudy.com", $header); + $this->assertStringNotContainsString("script-src 'self' cdn.cloudy.com nonce-", $header); + } + public function testBodyScriptNonceCustomScriptTag(): void { $config = new CSPConfig(); @@ -810,6 +830,26 @@ public function testBodyStyleNonce(): void $this->assertStringContainsString('nonce-', $header); } + public function testDisabledStyleNonce(): void + { + $this->csp->clearDirective('style-src'); + + $this->csp->setEnableStyleNonce(false); + $this->csp->addStyleSrc('self'); + $this->csp->addStyleSrc('cdn.cloudy.com'); + + $this->assertTrue($this->work('')); + + $header = $this->response->getHeaderLine('Content-Security-Policy'); + $body = $this->response->getBody(); + + $this->assertIsString($body); + $this->assertStringNotContainsString('nonce=', $body); + + $this->assertStringContainsString("style-src 'self' cdn.cloudy.com", $header); + $this->assertStringNotContainsString("style-src 'self' cdn.cloudy.com nonce-", $header); + } + public function testBodyStyleNonceCustomStyleTag(): void { $config = new CSPConfig(); diff --git a/user_guide_src/source/outgoing/csp.rst b/user_guide_src/source/outgoing/csp.rst index 0c62f3cee56d..f6abc8793aa6 100644 --- a/user_guide_src/source/outgoing/csp.rst +++ b/user_guide_src/source/outgoing/csp.rst @@ -171,3 +171,18 @@ In this case, you can use the functions, :php:func:`csp_script_nonce()` and :php + +.. _csp-control-nonce-generation: + +Control Nonce Generation +======================== + +.. versionadded:: 4.8.0 + +By default, both the script and style nonces are generated automatically. If you want to only generate one of them, +you can set ``$enableStyleNonce`` or ``$enableScriptNonce`` to false in **app/Config/ContentSecurityPolicy.php**: + +.. literalinclude:: csp/016.php + +By setting one of these to false, the corresponding nonce will not be generated, and the placeholder will be replaced with an empty string. +This gives you the flexibility to use nonces for only one type of content if you choose, without affecting the other. diff --git a/user_guide_src/source/outgoing/csp/016.php b/user_guide_src/source/outgoing/csp/016.php new file mode 100644 index 000000000000..b4063e2f4390 --- /dev/null +++ b/user_guide_src/source/outgoing/csp/016.php @@ -0,0 +1,14 @@ +