From 911f833ede61b7745f7ac2b578b05b2fbc1ea64f Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 14 Feb 2022 11:08:53 +0100 Subject: [PATCH] Improved Content-Security-Policy management for Captcha issue #222 --- plugins/README.md | 4 + plugins/recaptcha/index.php | 18 ++++- .../0.0.0/app/libraries/RainLoop/Service.php | 27 +++---- .../app/libraries/RainLoop/ServiceActions.php | 5 ++ .../app/libraries/snappymail/http/csp.php | 78 +++++++++++++++++++ 5 files changed, 113 insertions(+), 19 deletions(-) create mode 100644 snappymail/v/0.0.0/app/libraries/snappymail/http/csp.php diff --git a/plugins/README.md b/plugins/README.md index 79f781d66..cf9090541 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -301,6 +301,10 @@ $Plugin->addHook('hook.name', 'functionName'); \RainLoop\Model\Account $oAccount int $iLimit +### main.content-security-policy + params: + \SnappyMail\HTTP\CSP $oCSP + ### main.default-response params: string $sActionName diff --git a/plugins/recaptcha/index.php b/plugins/recaptcha/index.php index 0535c2c93..a0fd55ffe 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.12', - RELEASE = '2022-02-11', - REQUIRED = '2.12.0', + VERSION = '2.12.1', + RELEASE = '2022-02-14', + REQUIRED = '2.12.1', 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'; @@ -24,6 +24,7 @@ class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin $this->addHook('json.action-pre-call', 'AjaxActionPreCall'); $this->addHook('filter.json-response', 'FilterAjaxResponse'); + $this->addHook('main.content-security-policy', 'ContentSecurityPolicy'); } protected function configMapping() : array @@ -77,7 +78,7 @@ class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin public function FilterAppDataPluginSection(bool $bAdmin, bool $bAuth, array &$aConfig) : void { if (!$bAdmin && !$bAuth) { - $aConfig['show_captcha_on_login'] = 1; + $aConfig['show_captcha_on_login'] = 1 > $this->getLimit();; } } @@ -140,4 +141,13 @@ 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->frame[] = 'https://www.google.com/recaptcha/'; + $CSP->frame[] = 'https://recaptcha.google.com/recaptcha/'; + } + } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Service.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Service.php index dd808ca71..fcd24d520 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Service.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Service.php @@ -240,23 +240,20 @@ abstract class Service private static function setCSP(string $sScriptNonce = null) : void { - // "img-src https:" is allowed due to remote images in e-mails - $sContentSecurityPolicy = \trim(Api::Config()->Get('security', 'content_security_policy', '')) - ?: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: https: http:; style-src 'self' 'unsafe-inline'"; - if (Api::Config()->Get('security', 'use_local_proxy_for_external_images', '')) { - $sContentSecurityPolicy = \preg_replace('/(img-src[^;]+)\\shttps:(\\s|;|$)/D', '$1$2', $sContentSecurityPolicy); - $sContentSecurityPolicy = \preg_replace('/(img-src[^;]+)\\shttp:(\\s|;|$)/D', '$1$2', $sContentSecurityPolicy); + $CSP = new \SnappyMail\HTTP\CSP(\trim(Api::Config()->Get('security', 'content_security_policy', ''))); + $CSP->report_only = Api::Config()->Get('debug', 'enable', false); // '0.0.0' === APP_VERSION + // Allow https: due to remote images in e-mails or use proxy + if (!Api::Config()->Get('security', 'use_local_proxy_for_external_images', '')) { + $CSP->img[] = 'https:'; + $CSP->img[] = 'http:'; } // Internet Explorer does not support 'nonce' - if (!$_SERVER['HTTP_USER_AGENT'] || (!\strpos($_SERVER['HTTP_USER_AGENT'], 'Trident/') && !\strpos($_SERVER['HTTP_USER_AGENT'], 'Edge/1'))) { - if ($sScriptNonce) { - $sContentSecurityPolicy = \str_replace('script-src', "script-src 'nonce-{$sScriptNonce}'", $sContentSecurityPolicy); - } - // Knockout.js requires unsafe-inline? - $sContentSecurityPolicy = \preg_replace("/(script-src[^;]+)'unsafe-inline'/", '$1', $sContentSecurityPolicy); - // Knockout.js requires eval() for observable binding purposes - //$sContentSecurityPolicy = \preg_replace("/(script-src[^;]+)'unsafe-eval'/", '$1', $sContentSecurityPolicy); + if ($sScriptNonce && !$_SERVER['HTTP_USER_AGENT'] || (!\strpos($_SERVER['HTTP_USER_AGENT'], 'Trident/') && !\strpos($_SERVER['HTTP_USER_AGENT'], 'Edge/1'))) { + $CSP->script[] = "'nonce-{$sScriptNonce}'"; } - \header('Content-Security-Policy: '.$sContentSecurityPolicy); + + Api::Actions()->Plugins()->RunHook('main.content-security-policy', array($CSP)); + + $CSP->setHeaders(); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php index a2e4021b7..e006a7279 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php @@ -371,6 +371,11 @@ class ServiceActions return ''; } + public function ServiceCspReport() : void + { + \SnappyMail\HTTP\CSP::logReport(); + } + public function ServiceRaw() : string { $sResult = ''; 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 new file mode 100644 index 000000000..221a51a40 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/csp.php @@ -0,0 +1,78 @@ +$name = $values; + } + } + } + + function __toString() : string + { + $params = [ + 'default-src ' . \implode(' ', $this->default), + 'script-src ' . \implode(' ', $this->script), + 'img-src ' . \implode(' ', $this->img), + 'style-src ' . \implode(' ', $this->style), + ]; + if ($this->script) { + $params[] = 'script-src ' . \implode(' ', $this->script); + } + if ($this->img) { + $params[] = 'img-src ' . \implode(' ', $this->img); + } + if ($this->style) { + $params[] = 'style-src ' . \implode(' ', $this->style); + } + if ($this->frame) { + $params[] = 'frame-src ' . \implode(' ', $this->frame); + } + // Deprecated + $params[] = 'report-uri ./?/CspReport'; + + return \implode('; ', $params); + } + + public function setHeaders() : void + { + if ($this->report_only) { + \header('Content-Security-Policy-Report-Only: ' . $this); + } else { + \header('Content-Security-Policy: ' . $this); + } + } + + public static function logReport() : void + { + \http_response_code(204); + $json = \file_get_contents('php://input'); + $report = \json_decode($json, true); + // Useless to log 'moz-extension' as there's no clue which extension violates + if ($json && $report && 'moz-extension' !== $report['csp-report']['source-file']) { + \SnappyMail\Log::error('CSP', $json); + } + exit; + } +}