Improved Content-Security-Policy management for Captcha issue #222

This commit is contained in:
the-djmaze 2022-02-14 11:08:53 +01:00
parent b257fac19b
commit 911f833ede
5 changed files with 113 additions and 19 deletions

View file

@ -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

View file

@ -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/';
}
}

View file

@ -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();
}
}

View file

@ -371,6 +371,11 @@ class ServiceActions
return '';
}
public function ServiceCspReport() : void
{
\SnappyMail\HTTP\CSP::logReport();
}
public function ServiceRaw() : string
{
$sResult = '';

View file

@ -0,0 +1,78 @@
<?php
/**
* Controls the content_security_policy
*/
namespace SnappyMail\HTTP;
class CSP
{
public
$default = ["'self'"],
// Knockout.js requires unsafe-inline?
// Knockout.js requires eval() for observable binding purposes
$script = ["'self'", "'unsafe-eval'"/*, "'unsafe-inline'"*/],
$img = ["'self'", 'data:'],
$style = ["'self'", "'unsafe-inline'"],
$frame = [],
$report_to = [],
$report_only = false;
function __construct(string $default = '')
{
if ($default) {
foreach (\explode(';', $default) as $directive) {
$values = \explode(' ', $directive);
$name = \preg_replace('/-.+/', '', \trim(\array_shift($values)));
$this->$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;
}
}