diff --git a/app/Config/ContentSecurityPolicy.php b/app/Config/ContentSecurityPolicy.php
index 6fa5bd7b4cc7..aa18ba9f1060 100644
--- a/app/Config/ContentSecurityPolicy.php
+++ b/app/Config/ContentSecurityPolicy.php
@@ -164,4 +164,25 @@ class ContentSecurityPolicy extends BaseConfig
* @var string|string[]|null
*/
public $sandbox;
+
+ /**
+ * Nonce tag for style
+ *
+ * @var string
+ */
+ public $styleNonceTag = '{csp-style-nonce}';
+
+ /**
+ * Nonce tag for script
+ *
+ * @var string
+ */
+ public $scriptNonceTag = '{csp-script-nonce}';
+
+ /**
+ * Replace nonce tag automatically
+ *
+ * @var bool
+ */
+ public $autoNonce = true;
}
diff --git a/env b/env
index c60b367265e7..83def018081f 100644
--- a/env
+++ b/env
@@ -60,7 +60,7 @@
# contentsecuritypolicy.scriptSrc = 'self'
# contentsecuritypolicy.styleSrc = 'self'
# contentsecuritypolicy.imageSrc = 'self'
-# contentsecuritypolicy.base_uri = null
+# contentsecuritypolicy.baseURI = null
# contentsecuritypolicy.childSrc = null
# contentsecuritypolicy.connectSrc = 'self'
# contentsecuritypolicy.fontSrc = null
@@ -73,6 +73,9 @@
# contentsecuritypolicy.reportURI = null
# contentsecuritypolicy.sandbox = false
# contentsecuritypolicy.upgradeInsecureRequests = false
+# contentsecuritypolicy.styleNonceTag = '{csp-style-nonce}'
+# contentsecuritypolicy.scriptNonceTag = '{csp-script-nonce}'
+# contentsecuritypolicy.autoNonce = true
#--------------------------------------------------------------------
# COOKIE
diff --git a/system/Common.php b/system/Common.php
index e2fa9bce5a33..690275c4fedf 100644
--- a/system/Common.php
+++ b/system/Common.php
@@ -288,6 +288,38 @@ function csrf_meta(?string $id = null): string
}
}
+if (! function_exists('csp_style_nonce')) {
+ /**
+ * Generates a nonce attribute for style tag.
+ */
+ function csp_style_nonce(): string
+ {
+ $csp = Services::csp();
+
+ if (! $csp->enabled()) {
+ return '';
+ }
+
+ return 'nonce="' . $csp->getStyleNonce() . '"';
+ }
+}
+
+if (! function_exists('csp_script_nonce')) {
+ /**
+ * Generates a nonce attribute for script tag.
+ */
+ function csp_script_nonce(): string
+ {
+ $csp = Services::csp();
+
+ if (! $csp->enabled()) {
+ return '';
+ }
+
+ return 'nonce="' . $csp->getScriptNonce() . '"';
+ }
+}
+
if (! function_exists('db_connect')) {
/**
* Grabs a database connection and returns it to the user.
diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php
index b50617d7ed83..38bac37fdaa9 100644
--- a/system/Config/BaseService.php
+++ b/system/Config/BaseService.php
@@ -28,6 +28,7 @@
use CodeIgniter\Format\Format;
use CodeIgniter\Honeypot\Honeypot;
use CodeIgniter\HTTP\CLIRequest;
+use CodeIgniter\HTTP\ContentSecurityPolicy;
use CodeIgniter\HTTP\CURLRequest;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\Negotiate;
@@ -56,6 +57,7 @@
use Config\App;
use Config\Autoload;
use Config\Cache;
+use Config\ContentSecurityPolicy as CSPConfig;
use Config\Encryption;
use Config\Exceptions as ConfigExceptions;
use Config\Filters as ConfigFilters;
@@ -94,6 +96,7 @@
* @method static CLIRequest clirequest(App $config = null, $getShared = true)
* @method static CodeIgniter codeigniter(App $config = null, $getShared = true)
* @method static Commands commands($getShared = true)
+ * @method static ContentSecurityPolicy csp(CSPConfig $config = null, $getShared = true)
* @method static CURLRequest curlrequest($options = [], ResponseInterface $response = null, App $config = null, $getShared = true)
* @method static Email email($config = null, $getShared = true)
* @method static EncrypterInterface encrypter(Encryption $config = null, $getShared = false)
diff --git a/system/Config/Services.php b/system/Config/Services.php
index b72fad36eec7..2d44e3ccfd78 100644
--- a/system/Config/Services.php
+++ b/system/Config/Services.php
@@ -28,6 +28,7 @@
use CodeIgniter\Format\Format;
use CodeIgniter\Honeypot\Honeypot;
use CodeIgniter\HTTP\CLIRequest;
+use CodeIgniter\HTTP\ContentSecurityPolicy;
use CodeIgniter\HTTP\CURLRequest;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\Negotiate;
@@ -56,6 +57,7 @@
use CodeIgniter\View\View;
use Config\App;
use Config\Cache;
+use Config\ContentSecurityPolicy as CSPConfig;
use Config\Email as EmailConfig;
use Config\Encryption as EncryptionConfig;
use Config\Exceptions as ExceptionsConfig;
@@ -153,6 +155,22 @@ public static function commands(bool $getShared = true)
return new Commands();
}
+ /**
+ * Content Security Policy
+ *
+ * @return ContentSecurityPolicy
+ */
+ public static function csp(?CSPConfig $config = null, bool $getShared = true)
+ {
+ if ($getShared) {
+ return static::getSharedInstance('csp', $config);
+ }
+
+ $config ??= config('ContentSecurityPolicy');
+
+ return new ContentSecurityPolicy($config);
+ }
+
/**
* The CURL Request class acts as a simple HTTP client for interacting
* with other servers, typically through APIs.
diff --git a/system/Config/View.php b/system/Config/View.php
index 5a8baeac2d6a..da4f81b1c716 100644
--- a/system/Config/View.php
+++ b/system/Config/View.php
@@ -76,6 +76,8 @@ class View extends BaseConfig
* @var array
*/
protected $corePlugins = [
+ 'csp_script_nonce' => '\CodeIgniter\View\Plugins::cspScriptNonce',
+ 'csp_style_nonce' => '\CodeIgniter\View\Plugins::cspStyleNonce',
'current_url' => '\CodeIgniter\View\Plugins::currentURL',
'previous_url' => '\CodeIgniter\View\Plugins::previousURL',
'mailto' => '\CodeIgniter\View\Plugins::mailto',
diff --git a/system/Debug/Kint/RichRenderer.php b/system/Debug/Kint/RichRenderer.php
index 756cac75e144..8210fb21e1c2 100644
--- a/system/Debug/Kint/RichRenderer.php
+++ b/system/Debug/Kint/RichRenderer.php
@@ -36,11 +36,11 @@ public function preRender()
switch ($type) {
case 'script':
- $output .= '';
+ $output .= '';
break;
case 'style':
- $output .= '';
+ $output .= '';
break;
default:
diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php
index 8e100fe25a6b..bf502f1f1563 100644
--- a/system/Debug/Toolbar.php
+++ b/system/Debug/Toolbar.php
@@ -402,11 +402,11 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r
$kintScript = substr($kintScript, 0, strpos($kintScript, '') + 8);
$script = PHP_EOL
- . ''
- . ''
- . ''
+ . ''
+ . ''
. $kintScript
. PHP_EOL;
diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php
index 3b18fc8bf1df..b7f9b066d7d8 100644
--- a/system/HTTP/ContentSecurityPolicy.php
+++ b/system/HTTP/ContentSecurityPolicy.php
@@ -176,6 +176,41 @@ class ContentSecurityPolicy
*/
protected $nonces = [];
+ /**
+ * Nonce for style
+ *
+ * @var string
+ */
+ protected $styleNonce;
+
+ /**
+ * Nonce for script
+ *
+ * @var string
+ */
+ protected $scriptNonce;
+
+ /**
+ * Nonce tag for style
+ *
+ * @var string
+ */
+ protected $styleNonceTag = '{csp-style-nonce}';
+
+ /**
+ * Nonce tag for script
+ *
+ * @var string
+ */
+ protected $scriptNonceTag = '{csp-script-nonce}';
+
+ /**
+ * Replace nonce tag automatically
+ *
+ * @var bool
+ */
+ protected $autoNonce = true;
+
/**
* An array of header info since we have
* to build ourself before passing to Response.
@@ -192,6 +227,13 @@ class ContentSecurityPolicy
*/
protected $reportOnlyHeaders = [];
+ /**
+ * Whether Content Security Policy is being enforced.
+ *
+ * @var bool
+ */
+ protected $CSPEnabled = false;
+
/**
* Constructor.
*
@@ -199,11 +241,56 @@ class ContentSecurityPolicy
*/
public function __construct(ContentSecurityPolicyConfig $config)
{
+ $appConfig = config('App');
+ $this->CSPEnabled = $appConfig->CSPEnabled;
+
foreach (get_object_vars($config) as $setting => $value) {
if (property_exists($this, $setting)) {
$this->{$setting} = $value;
}
}
+
+ if (! is_array($this->styleSrc)) {
+ $this->styleSrc = [$this->styleSrc];
+ }
+
+ if (! is_array($this->scriptSrc)) {
+ $this->scriptSrc = [$this->scriptSrc];
+ }
+ }
+
+ /**
+ * Whether Content Security Policy is being enforced.
+ */
+ public function enabled(): bool
+ {
+ return $this->CSPEnabled;
+ }
+
+ /**
+ * Get the nonce for the style tag.
+ */
+ public function getStyleNonce(): string
+ {
+ if ($this->styleNonce === null) {
+ $this->styleNonce = bin2hex(random_bytes(12));
+ $this->styleSrc[] = 'nonce-' . $this->styleNonce;
+ }
+
+ return $this->styleNonce;
+ }
+
+ /**
+ * Get the nonce for the script tag.
+ */
+ public function getScriptNonce(): string
+ {
+ if ($this->scriptNonce === null) {
+ $this->scriptNonce = bin2hex(random_bytes(12));
+ $this->scriptSrc[] = 'nonce-' . $this->scriptNonce;
+ }
+
+ return $this->scriptNonce;
}
/**
@@ -213,6 +300,10 @@ public function __construct(ContentSecurityPolicyConfig $config)
*/
public function finalize(ResponseInterface &$response)
{
+ if ($this->autoNonce === false) {
+ return;
+ }
+
$this->generateNonces($response);
$this->buildHeaders($response);
}
@@ -580,28 +671,18 @@ protected function generateNonces(ResponseInterface &$response)
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;
+ $pattern = '/' . preg_quote($this->styleNonceTag, '/') . '/';
+ $body = preg_replace_callback($pattern, function () {
+ $nonce = $this->getStyleNonce();
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;
+ $pattern = '/' . preg_quote($this->scriptNonceTag, '/') . '/';
+ $body = preg_replace_callback($pattern, function () {
+ $nonce = $this->getScriptNonce();
return "nonce=\"{$nonce}\"";
}, $body);
diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php
index 8e2cc472cc00..493c990e1eae 100644
--- a/system/HTTP/Response.php
+++ b/system/HTTP/Response.php
@@ -16,7 +16,7 @@
use CodeIgniter\Cookie\Exceptions\CookieException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use Config\App;
-use Config\ContentSecurityPolicy as CSPConfig;
+use Config\Services;
/**
* Representation of an outgoing, getServer-side response.
@@ -152,7 +152,7 @@ public function __construct($config)
$this->noCache();
// We need CSP object even if not enabled to avoid calls to non existing methods
- $this->CSP = new ContentSecurityPolicy(new CSPConfig());
+ $this->CSP = Services::csp();
$this->CSPEnabled = $config->CSPEnabled;
diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php
index 278143fd3c3a..412e7b970e7c 100644
--- a/system/HTTP/ResponseTrait.php
+++ b/system/HTTP/ResponseTrait.php
@@ -38,6 +38,8 @@ trait ResponseTrait
* Whether Content Security Policy is being enforced.
*
* @var bool
+ *
+ * @deprecated Use $this->CSP->enabled() instead.
*/
protected $CSPEnabled = false;
@@ -433,7 +435,7 @@ public function send()
{
// If we're enforcing a Content Security Policy,
// we need to give it a chance to build out it's headers.
- if ($this->CSPEnabled === true) {
+ if ($this->CSP->enabled()) {
$this->CSP->finalize($this);
} else {
$this->body = str_replace(['{csp-style-nonce}', '{csp-script-nonce}'], '', $this->body ?? '');
diff --git a/system/View/Plugins.php b/system/View/Plugins.php
index d0e170071a6a..b74ad96bd385 100644
--- a/system/View/Plugins.php
+++ b/system/View/Plugins.php
@@ -103,4 +103,20 @@ public static function siteURL(array $params = []): string
{
return site_url(...$params);
}
+
+ /**
+ * Wrap csp_script_nonce() function to use as view plugin.
+ */
+ public static function cspScriptNonce(): string
+ {
+ return csp_script_nonce();
+ }
+
+ /**
+ * Wrap csp_style_nonce() function to use as view plugin.
+ */
+ public static function cspStyleNonce(): string
+ {
+ return csp_style_nonce();
+ }
}
diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php
index 3916a1ab785f..15a511926d3c 100644
--- a/tests/system/CommonFunctionsTest.php
+++ b/tests/system/CommonFunctionsTest.php
@@ -519,19 +519,19 @@ public function testIsCli()
public function testDWithCSP()
{
+ $this->resetServices();
+
/** @var App $config */
- $config = config(App::class);
- $CSPEnabled = $config->CSPEnabled;
+ $config = config('App');
$cliDetection = Kint::$cli_detection;
$config->CSPEnabled = true;
Kint::$cli_detection = false;
- $this->expectOutputRegex('/
+
+ // Becomes
+
+
+ // OR
+
+
Class Reference
===============
diff --git a/user_guide_src/source/outgoing/view_parser.rst b/user_guide_src/source/outgoing/view_parser.rst
index e182ce789b35..9aae57f1af00 100644
--- a/user_guide_src/source/outgoing/view_parser.rst
+++ b/user_guide_src/source/outgoing/view_parser.rst
@@ -530,6 +530,10 @@ lang language string Alias for the lang helper function.
validation_errors fieldname(optional) Returns either error string for the field {+ validation_errors +} , {+ validation_errors field="email" +}
(if specified) or all validation errors.
route route name Alias for the route_to helper function. {+ route "login" +}
+csp_script_nonce Alias for the csp_script_nonce helper {+ csp_script_nonce +}
+ function.
+csp_style_nonce Alias for the csp_style_nonce helper {+ csp_style_nonce +}
+ function.
================== ========================= ============================================ ================================================================
Registering a Plugin