Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: CSP enhancements #5516

Merged
merged 24 commits into from
Jan 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f43a6b4
refactor: move $CSPEnabled from Response to ContentSecurityPolicy
kenjis Dec 31, 2021
434f90a
feat: add methods to get nonces
kenjis Dec 31, 2021
c45cfcb
refactor: output only one nonce
kenjis Dec 31, 2021
6be16a8
feat: custom nonce tags
kenjis Dec 31, 2021
7a75b89
feat: add csp_style_nonce() and csp_script_nonce()
kenjis Dec 31, 2021
a08d7e5
refactor: use csp_script_nonce() and csp_script_nonce() in Debug Toolbar
kenjis Dec 31, 2021
e7b0727
config: fix incorrect property name
kenjis Dec 31, 2021
d548118
feat: add config for disabling to replace nonce tag automatically
kenjis Dec 31, 2021
bc52fad
refactor: use csp_script_nonce() and csp_script_nonce() in Kint
kenjis Dec 31, 2021
9b1f513
feat: add Services::contentsecuritypolicy()
kenjis Dec 31, 2021
9ece590
refactor: use Services::contentsecuritypolicy()
kenjis Dec 31, 2021
031e411
fix: CSP nonce header is nottt sent when CSP tag replacement does not…
kenjis Dec 31, 2021
571a34c
test: make regex strict
kenjis Dec 31, 2021
3fde024
refactor: remove uneeded use statement
kenjis Jan 4, 2022
c4aaa4f
refactor: rename from contentsecuritypolicy() to csp()
kenjis Jan 4, 2022
4b8b118
test: remove unneeded $this->resetFactories()
kenjis Jan 4, 2022
b1e91dd
feat: add csp_script_nonce and csp_style_nonce plugin for View Parser
kenjis Jan 4, 2022
c472bb6
docs: update user guide
kenjis Jan 5, 2022
dee40c1
docs: fix by proofreading
kenjis Jan 6, 2022
ae90f8e
docs: fix by proofreading
kenjis Jan 6, 2022
161be10
docs: fix by proofreading
kenjis Jan 6, 2022
1184046
test: use more specific assert method
kenjis Jan 6, 2022
f686c25
docs: add changelogs/v4.2.0
kenjis Jan 6, 2022
444da2b
refactor: fix by rector
kenjis Jan 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/Config/ContentSecurityPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
5 changes: 4 additions & 1 deletion env
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
32 changes: 32 additions & 0 deletions system/Common.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions system/Config/BaseService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions system/Config/Services.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions system/Config/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions system/Debug/Kint/RichRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ public function preRender()

switch ($type) {
case 'script':
$output .= '<script {csp-script-nonce} class="kint-rich-script">' . $contents . '</script>';
$output .= '<script ' . csp_script_nonce() . ' class="kint-rich-script">' . $contents . '</script>';
MGatner marked this conversation as resolved.
Show resolved Hide resolved
break;

case 'style':
$output .= '<style {csp-style-nonce} class="kint-rich-style">' . $contents . '</style>';
$output .= '<style ' . csp_style_nonce() . ' class="kint-rich-style">' . $contents . '</style>';
break;

default:
Expand Down
6 changes: 3 additions & 3 deletions system/Debug/Toolbar.php
Original file line number Diff line number Diff line change
Expand Up @@ -402,11 +402,11 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r
$kintScript = substr($kintScript, 0, strpos($kintScript, '</style>') + 8);

$script = PHP_EOL
. '<script type="text/javascript" {csp-script-nonce} id="debugbar_loader" '
. '<script type="text/javascript" ' . csp_script_nonce() . ' id="debugbar_loader" '
. 'data-time="' . $time . '" '
. 'src="' . site_url() . '?debugbar"></script>'
. '<script type="text/javascript" {csp-script-nonce} id="debugbar_dynamic_script"></script>'
. '<style type="text/css" {csp-style-nonce} id="debugbar_dynamic_style"></style>'
. '<script type="text/javascript" ' . csp_script_nonce() . ' id="debugbar_dynamic_script"></script>'
. '<style type="text/css" ' . csp_style_nonce() . ' id="debugbar_dynamic_style"></style>'
. $kintScript
. PHP_EOL;

Expand Down
113 changes: 97 additions & 16 deletions system/HTTP/ContentSecurityPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -192,18 +227,70 @@ class ContentSecurityPolicy
*/
protected $reportOnlyHeaders = [];

/**
* Whether Content Security Policy is being enforced.
*
* @var bool
*/
protected $CSPEnabled = false;

/**
* Constructor.
*
* Stores our default values from the Config file.
*/
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;
}

/**
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions system/HTTP/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;

Expand Down
4 changes: 3 additions & 1 deletion system/HTTP/ResponseTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ trait ResponseTrait
* Whether Content Security Policy is being enforced.
*
* @var bool
*
* @deprecated Use $this->CSP->enabled() instead.
*/
protected $CSPEnabled = false;

Expand Down Expand Up @@ -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 ?? '');
Expand Down
Loading