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; + } +}