From bb77d0ae024bb98d03ee3169085a238b99dad680 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 22 Feb 2023 13:57:37 +0100 Subject: [PATCH] Redesign \SnappyMail\HTTP\CSP for better control --- plugins/recaptcha/index.php | 15 ++-- .../v/0.0.0/app/libraries/RainLoop/Api.php | 7 +- .../app/libraries/snappymail/http/csp.php | 75 +++++++++---------- 3 files changed, 44 insertions(+), 53 deletions(-) diff --git a/plugins/recaptcha/index.php b/plugins/recaptcha/index.php index d4935b8e8..9d76e4a7d 100644 --- a/plugins/recaptcha/index.php +++ b/plugins/recaptcha/index.php @@ -6,9 +6,9 @@ class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin NAME = 'reCaptcha', AUTHOR = 'SnappyMail', URL = 'https://snappymail.eu/', - VERSION = '2.14', - RELEASE = '2023-01-11', - REQUIRED = '2.23', + VERSION = '2.15', + RELEASE = '2023-02-22', + REQUIRED = '2.26.4', CATEGORY = 'General', LICENSE = 'MIT', DESCRIPTION = 'A CAPTCHA (v2) is a program that can generate and grade tests that humans can pass but current computer programs cannot. For example, humans can read distorted text as the one shown below, but current computer programs can\'t. More info at https://developers.google.com/recaptcha'; @@ -141,12 +141,9 @@ class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin public function ContentSecurityPolicy(\SnappyMail\HTTP\CSP $CSP) { -// $CSP->script[] = 'https://www.google.com/recaptcha/'; - $CSP->script[] = 'https://www.gstatic.com/recaptcha/'; - $CSP->script[] = 'https://www.recaptcha.net/recaptcha/'; -// $CSP->frame[] = 'https://www.google.com/recaptcha/'; -// $CSP->frame[] = 'https://recaptcha.google.com/recaptcha/'; - $CSP->frame[] = 'https://www.recaptcha.net/recaptcha/'; + $CSP->add('script-src', 'https://www.gstatic.com/recaptcha/'); + $CSP->add('script-src', 'https://www.recaptcha.net/recaptcha/'); + $CSP->add('frame-src', 'https://www.recaptcha.net/recaptcha/'); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php index 783465e8d..16200a64e 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php @@ -41,15 +41,14 @@ abstract class Api $CSP = new \SnappyMail\HTTP\CSP(\trim($oConfig->Get('security', 'content_security_policy', ''))); $CSP->report = $oConfig->Get('security', 'csp_report', false); $CSP->report_only = $oConfig->Get('debug', 'enable', false); // || SNAPPYMAIL_DEV -// $CSP->frame = \explode(' ', $oConfig->Get('security', 'csp_frame', '')); // Allow https: due to remote images in e-mails or use proxy if (!$oConfig->Get('security', 'use_local_proxy_for_external_images', '')) { - $CSP->img[] = 'https:'; - $CSP->img[] = 'http:'; + $CSP->add('img-src', 'https:'); + $CSP->add('img-src', 'http:'); } if ($sScriptNonce) { - $CSP->script[] = "'nonce-{$sScriptNonce}'"; + $CSP->add('script-src', "'nonce-{$sScriptNonce}'"); } static::Actions()->Plugins()->RunHook('main.content-security-policy', array($CSP)); diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/csp.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/csp.php index cc828ee49..5141fce03 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/http/csp.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/csp.php @@ -8,64 +8,59 @@ namespace SnappyMail\HTTP; class CSP { public - $base = ["'self'"], - $default = ["'self'", 'data:'], - // Knockout.js requires eval() for observable binding purposes - // Safari < 15.4 does not support strict-dynamic -// $script = ["'strict-dynamic'", "'unsafe-eval'"], - $script = ["'self'", "'unsafe-eval'"], - // Knockout.js requires unsafe-inline? -// $script = ["'self'", "'unsafe-inline'", "'unsafe-eval'"], - $img = ["'self'", 'data:'], - $style = ["'self'", "'unsafe-inline'"], - $frame = [], - $frame_ancestors = [], - $report = false, $report_to = [], $report_only = false; + private $directives = [ + 'base-uri' => ["'self'"], + 'default-src' => ["'self'", 'data:'], + // Knockout.js requires eval() for observable binding purposes + // Safari < 15.4 does not support strict-dynamic +// 'script-src' => ["'strict-dynamic'", "'unsafe-eval'"], + 'script-src' => ["'self'", "'unsafe-eval'"], + // Knockout.js requires unsafe-inline? +// 'script-src' => ["'self'", "'unsafe-inline'", "'unsafe-eval'"], + 'img-src' => ["'self'", 'data:'], + 'style-src' => ["'self'", "'unsafe-inline'"], + ]; + function __construct(string $default = '') { if ($default) { foreach (\explode(';', $default) as $directive) { - $values = \preg_split('/\\s+/', $directive); - $name = \str_replace('-', '_', \preg_replace('/-(src|uri)$/D', '', \trim(\array_shift($values)))); - $this->$name = \array_unique(\array_merge($this->$name, $values)); + $sources = \preg_split('/\\s+/', \trim($directive)); + $directive = \array_shift($sources); + if (!isset($this->directives[$directive])) { + $this->directives[$directive] = []; + } + $this->directives[$directive] = \array_merge($this->directives[$directive], $sources); } } } function __toString() : string { - $params = [ - 'base-uri ' . \implode(' ', \array_unique($this->base)), - 'default-src ' . \implode(' ', \array_unique($this->default)) - ]; - if ($this->script) { - $params[] = 'script-src ' . \implode(' ', \array_unique($this->script)); - } - if ($this->img) { - $params[] = 'img-src ' . \implode(' ', \array_unique($this->img)); - } - if ($this->style) { - $params[] = 'style-src ' . \implode(' ', \array_unique($this->style)); - } - if ($this->frame) { - $params[] = 'frame-src ' . \implode(' ', \array_unique($this->frame)); - } - if ($this->frame_ancestors) { - $params[] = 'frame-ancestors ' . \implode(' ', \array_unique($this->frame_ancestors)); - } - - // Deprecated + // report-uri deprecated + unset($this->directives['report-uri']); if ($this->report) { - $params[] = 'report-uri ./?/CspReport'; + $this->directives['report-uri'] = [\RainLoop\Utils::WebPath() . '?/CspReport']; + } + $params = []; + foreach ($this->directives as $directive => $sources) { + $params[] = $directive . ' ' . \implode(' ', \array_unique($sources)); } - return \implode('; ', $params); } + public function add(string $directive, string $source) : void + { + if (!isset($this->directives[$directive])) { + $this->directives[$directive] = []; + } + $this->directives[$directive][] = $source; + } + public function setHeaders() : void { if ($this->report_only) { @@ -73,7 +68,7 @@ class CSP } else { \header('Content-Security-Policy: ' . $this); } - if (!$this->frame_ancestors) { + if (empty($this->directives['frame-ancestors'])) { \header('X-Frame-Options: DENY'); } else { // \header('X-Frame-Options: SAMEORIGIN');