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 @@
+