Skip to content

Commit 1ce26b2

Browse files
authored
feat: Add granular nonces (#10053)
1 parent 3e14cd1 commit 1ce26b2

File tree

8 files changed

+164
-6
lines changed

8 files changed

+164
-6
lines changed

app/Config/ContentSecurityPolicy.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,16 @@ class ContentSecurityPolicy extends BaseConfig
199199
*/
200200
public $sandbox;
201201

202+
/**
203+
* Enable nonce to style tags?
204+
*/
205+
public bool $enableStyleNonce = true;
206+
207+
/**
208+
* Enable nonce to script tags?
209+
*/
210+
public bool $enableScriptNonce = true;
211+
202212
/**
203213
* Nonce placeholder for style tags.
204214
*/

system/Autoloader/Autoloader.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,8 +460,11 @@ private function configureKint(): void
460460
}
461461

462462
$csp = service('csp');
463-
if ($csp->enabled()) {
464-
RichRenderer::$js_nonce = $csp->getScriptNonce();
463+
if ($csp->scriptNonceEnabled()) {
464+
RichRenderer::$js_nonce = $csp->getScriptNonce();
465+
}
466+
467+
if ($csp->styleNonceEnabled()) {
465468
RichRenderer::$css_nonce = $csp->getStyleNonce();
466469
}
467470

system/Common.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ function csp_style_nonce(): string
335335
{
336336
$csp = service('csp');
337337

338-
if (! $csp->enabled()) {
338+
if (! $csp->styleNonceEnabled()) {
339339
return '';
340340
}
341341

@@ -351,7 +351,7 @@ function csp_script_nonce(): string
351351
{
352352
$csp = service('csp');
353353

354-
if (! $csp->enabled()) {
354+
if (! $csp->scriptNonceEnabled()) {
355355
return '';
356356
}
357357

system/HTTP/ContentSecurityPolicy.php

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,16 @@ class ContentSecurityPolicy
298298
*/
299299
protected $scriptNonce;
300300

301+
/**
302+
* Whether to enable nonce to style-src and style-src-elem directives or not.
303+
*/
304+
protected bool $enableStyleNonce = true;
305+
306+
/**
307+
* Whether to enable nonce to script-src and script-src-elem directives or not.
308+
*/
309+
protected bool $enableScriptNonce = true;
310+
301311
/**
302312
* Nonce placeholder for style tags.
303313
*
@@ -392,11 +402,33 @@ public function enabled(): bool
392402
return $this->CSPEnabled;
393403
}
394404

405+
/**
406+
* Whether adding nonce in style-* directives is enabled or not.
407+
*/
408+
public function styleNonceEnabled(): bool
409+
{
410+
return $this->enabled() && $this->enableStyleNonce;
411+
}
412+
413+
/**
414+
* Whether adding nonce in script-* directives is enabled or not.
415+
*/
416+
public function scriptNonceEnabled(): bool
417+
{
418+
return $this->enabled() && $this->enableScriptNonce;
419+
}
420+
395421
/**
396422
* Get the nonce for the style tag.
397423
*/
398424
public function getStyleNonce(): string
399425
{
426+
if (! $this->enableStyleNonce) {
427+
$this->styleNonce = null;
428+
429+
return '';
430+
}
431+
400432
if ($this->styleNonce === null) {
401433
$this->styleNonce = base64_encode(random_bytes(12));
402434
$this->addStyleSrc('nonce-' . $this->styleNonce);
@@ -414,6 +446,12 @@ public function getStyleNonce(): string
414446
*/
415447
public function getScriptNonce(): string
416448
{
449+
if (! $this->enableScriptNonce) {
450+
$this->scriptNonce = null;
451+
452+
return '';
453+
}
454+
417455
if ($this->scriptNonce === null) {
418456
$this->scriptNonce = base64_encode(random_bytes(12));
419457
$this->addScriptSrc('nonce-' . $this->scriptNonce);
@@ -868,6 +906,30 @@ public function addReportingEndpoints(array $endpoint): static
868906
return $this;
869907
}
870908

909+
/**
910+
* Enables or disables adding nonces to style-src and style-src-elem directives.
911+
*
912+
* @return $this
913+
*/
914+
public function setEnableStyleNonce(bool $value = true): static
915+
{
916+
$this->enableStyleNonce = $value;
917+
918+
return $this;
919+
}
920+
921+
/**
922+
* Enables or disables adding nonces to script-src and script-src-elem directives.
923+
*
924+
* @return $this
925+
*/
926+
public function setEnableScriptNonce(bool $value = true): static
927+
{
928+
$this->enableScriptNonce = $value;
929+
930+
return $this;
931+
}
932+
871933
/**
872934
* DRY method to add an string or array to a class property.
873935
*
@@ -919,8 +981,21 @@ protected function generateNonces(ResponseInterface $response)
919981
return '';
920982
}
921983

922-
$nonce = $match[0] === $this->styleNonceTag ? $this->getStyleNonce() : $this->getScriptNonce();
923-
$attr = 'nonce="' . $nonce . '"';
984+
if ($match[0] === $this->styleNonceTag) {
985+
if (! $this->enableStyleNonce) {
986+
return '';
987+
}
988+
989+
$nonce = $this->getStyleNonce();
990+
} else {
991+
if (! $this->enableScriptNonce) {
992+
return '';
993+
}
994+
995+
$nonce = $this->getScriptNonce();
996+
}
997+
998+
$attr = 'nonce="' . $nonce . '"';
924999

9251000
return $jsonEscape ? str_replace('"', '\\"', $attr) : $attr;
9261001
}, $body);

tests/system/HTTP/ContentSecurityPolicyTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,26 @@ public function testBodyScriptNonce(): void
731731
$this->assertStringContainsString('nonce-', $header);
732732
}
733733

734+
public function testDisabledScriptNonce(): void
735+
{
736+
$this->csp->clearDirective('script-src');
737+
738+
$this->csp->setEnableScriptNonce(false);
739+
$this->csp->addScriptSrc('self');
740+
$this->csp->addScriptSrc('cdn.cloudy.com');
741+
742+
$this->assertTrue($this->work('<script {csp-script-nonce}></script>'));
743+
744+
$header = $this->response->getHeaderLine('Content-Security-Policy');
745+
$body = $this->response->getBody();
746+
747+
$this->assertIsString($body);
748+
$this->assertStringNotContainsString('nonce=', $body);
749+
750+
$this->assertStringContainsString("script-src 'self' cdn.cloudy.com", $header);
751+
$this->assertStringNotContainsString("script-src 'self' cdn.cloudy.com nonce-", $header);
752+
}
753+
734754
public function testBodyScriptNonceCustomScriptTag(): void
735755
{
736756
$config = new CSPConfig();
@@ -810,6 +830,26 @@ public function testBodyStyleNonce(): void
810830
$this->assertStringContainsString('nonce-', $header);
811831
}
812832

833+
public function testDisabledStyleNonce(): void
834+
{
835+
$this->csp->clearDirective('style-src');
836+
837+
$this->csp->setEnableStyleNonce(false);
838+
$this->csp->addStyleSrc('self');
839+
$this->csp->addStyleSrc('cdn.cloudy.com');
840+
841+
$this->assertTrue($this->work('<style {csp-style-nonce}></style>'));
842+
843+
$header = $this->response->getHeaderLine('Content-Security-Policy');
844+
$body = $this->response->getBody();
845+
846+
$this->assertIsString($body);
847+
$this->assertStringNotContainsString('nonce=', $body);
848+
849+
$this->assertStringContainsString("style-src 'self' cdn.cloudy.com", $header);
850+
$this->assertStringNotContainsString("style-src 'self' cdn.cloudy.com nonce-", $header);
851+
}
852+
813853
public function testBodyStyleNonceCustomStyleTag(): void
814854
{
815855
$config = new CSPConfig();

user_guide_src/source/changelogs/v4.8.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ HTTP
201201
For example: ``php index.php command -- --myarg`` will pass ``--myarg`` as an argument instead of an option.
202202
- ``CLIRequest`` now supports options with values specified using an equals sign (e.g., ``--option=value``) in addition to the existing space-separated syntax (e.g., ``--option value``).
203203
This provides more flexibility in how you can pass options to CLI requests.
204+
- Added ``$enableStyleNonce`` and ``$enableScriptNonce`` options to ``Config\App`` to automatically add nonces to control whether to add nonces to style-* and script-* directives in the Content Security Policy (CSP) header when CSP is enabled. See :ref:`csp-control-nonce-generation` for details.
204205

205206
Validation
206207
==========

user_guide_src/source/outgoing/csp.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,18 @@ In this case, you can use the functions, :php:func:`csp_script_nonce()` and :php
171171
<style <?= csp_style_nonce() ?>>
172172
. . .
173173
</style>
174+
175+
.. _csp-control-nonce-generation:
176+
177+
Control Nonce Generation
178+
========================
179+
180+
.. versionadded:: 4.8.0
181+
182+
By default, both the script and style nonces are generated automatically. If you want to only generate one of them,
183+
you can set ``$enableStyleNonce`` or ``$enableScriptNonce`` to false in **app/Config/ContentSecurityPolicy.php**:
184+
185+
.. literalinclude:: csp/016.php
186+
187+
By setting one of these to false, the corresponding nonce will not be generated, and the placeholder will be replaced with an empty string.
188+
This gives you the flexibility to use nonces for only one type of content if you choose, without affecting the other.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Config;
4+
5+
use CodeIgniter\Config\BaseConfig;
6+
7+
class ContentSecurityPolicy extends BaseConfig
8+
{
9+
// ...
10+
11+
public bool $enableStyleNonce = false;
12+
13+
public bool $enableScriptNonce = false;
14+
}

0 commit comments

Comments
 (0)