diff options
Diffstat (limited to 'lib')
6 files changed, 73 insertions, 8 deletions
diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 21d5eaa9503..97faa0edf49 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -379,7 +379,8 @@ class DIContainer extends SimpleContainer implements IAppContainer { $c['AppName'], $app->isLoggedIn(), $app->isAdminUser(), - $app->getServer()->getContentSecurityPolicyManager() + $app->getServer()->getContentSecurityPolicyManager(), + $app->getServer()->getCsrfTokenManager() ); }); diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php index 5e253d0954a..6c33c0023ea 100644 --- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php @@ -36,6 +36,7 @@ use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException; use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException; use OC\AppFramework\Utility\ControllerMethodReflector; use OC\Security\CSP\ContentSecurityPolicyManager; +use OC\Security\CSRF\CsrfTokenManager; use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\EmptyContentSecurityPolicy; use OCP\AppFramework\Http\RedirectResponse; @@ -77,6 +78,8 @@ class SecurityMiddleware extends Middleware { private $isAdminUser; /** @var ContentSecurityPolicyManager */ private $contentSecurityPolicyManager; + /** @var CsrfTokenManager */ + private $csrfTokenManager; /** * @param IRequest $request @@ -88,6 +91,7 @@ class SecurityMiddleware extends Middleware { * @param bool $isLoggedIn * @param bool $isAdminUser * @param ContentSecurityPolicyManager $contentSecurityPolicyManager + * @param CSRFTokenManager $csrfTokenManager */ public function __construct(IRequest $request, ControllerMethodReflector $reflector, @@ -97,7 +101,8 @@ class SecurityMiddleware extends Middleware { $appName, $isLoggedIn, $isAdminUser, - ContentSecurityPolicyManager $contentSecurityPolicyManager) { + ContentSecurityPolicyManager $contentSecurityPolicyManager, + CsrfTokenManager $csrfTokenManager) { $this->navigationManager = $navigationManager; $this->request = $request; $this->reflector = $reflector; @@ -107,6 +112,7 @@ class SecurityMiddleware extends Middleware { $this->isLoggedIn = $isLoggedIn; $this->isAdminUser = $isAdminUser; $this->contentSecurityPolicyManager = $contentSecurityPolicyManager; + $this->csrfTokenManager = $csrfTokenManager; } @@ -171,6 +177,23 @@ class SecurityMiddleware extends Middleware { } + private function browserSupportsCspV3() { + $browserWhitelist = [ + // Chrome 40+ + '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[4-9][0-9].[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+$/', + // Firefox 45+ + '/^Mozilla\/5\.0 \([^)]+\) Gecko\/[0-9.]+ Firefox\/(4[5-9]|[5-9][0-9])\.[0-9.]+$/', + // Safari 10+ + '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/1[0-9.]+ Safari\/[0-9.A-Z]+$/', + ]; + + if($this->request->isUserAgent($browserWhitelist)) { + return true; + } + + return false; + } + /** * Performs the default CSP modifications that may be injected by other * applications @@ -190,6 +213,10 @@ class SecurityMiddleware extends Middleware { $defaultPolicy = $this->contentSecurityPolicyManager->getDefaultPolicy(); $defaultPolicy = $this->contentSecurityPolicyManager->mergePolicies($defaultPolicy, $policy); + if($this->browserSupportsCspV3()) { + $defaultPolicy->useJsNonce($this->csrfTokenManager->getToken()->getEncryptedValue()); + } + $response->setContentSecurityPolicy($defaultPolicy); return $response; diff --git a/lib/private/Security/CSRF/CsrfToken.php b/lib/private/Security/CSRF/CsrfToken.php index bf61e339f77..dce9a83b727 100644 --- a/lib/private/Security/CSRF/CsrfToken.php +++ b/lib/private/Security/CSRF/CsrfToken.php @@ -33,6 +33,8 @@ namespace OC\Security\CSRF; class CsrfToken { /** @var string */ private $value; + /** @var string */ + private $encryptedValue = ''; /** * @param string $value Value of the token. Can be encrypted or not encrypted. @@ -48,8 +50,12 @@ class CsrfToken { * @return string */ public function getEncryptedValue() { - $sharedSecret = base64_encode(random_bytes(strlen($this->value))); - return base64_encode($this->value ^ $sharedSecret) .':'.$sharedSecret; + if($this->encryptedValue === '') { + $sharedSecret = base64_encode(random_bytes(strlen($this->value))); + $this->encryptedValue = base64_encode($this->value ^ $sharedSecret) . ':' . $sharedSecret; + } + + return $this->encryptedValue; } /** diff --git a/lib/private/Security/CSRF/CsrfTokenManager.php b/lib/private/Security/CSRF/CsrfTokenManager.php index d621cc2c29f..b43ca3d3679 100644 --- a/lib/private/Security/CSRF/CsrfTokenManager.php +++ b/lib/private/Security/CSRF/CsrfTokenManager.php @@ -34,6 +34,8 @@ class CsrfTokenManager { private $tokenGenerator; /** @var SessionStorage */ private $sessionStorage; + /** @var CsrfToken|null */ + private $csrfToken = null; /** * @param CsrfTokenGenerator $tokenGenerator @@ -51,6 +53,10 @@ class CsrfTokenManager { * @return CsrfToken */ public function getToken() { + if(!is_null($this->csrfToken)) { + return $this->csrfToken; + } + if($this->sessionStorage->hasToken()) { $value = $this->sessionStorage->getToken(); } else { @@ -58,7 +64,8 @@ class CsrfTokenManager { $this->sessionStorage->setToken($value); } - return new CsrfToken($value); + $this->csrfToken = new CsrfToken($value); + return $this->csrfToken; } /** @@ -69,13 +76,15 @@ class CsrfTokenManager { public function refreshToken() { $value = $this->tokenGenerator->generateToken(); $this->sessionStorage->setToken($value); - return new CsrfToken($value); + $this->csrfToken = new CsrfToken($value); + return $this->csrfToken; } /** * Remove the current token from the storage. */ public function removeToken() { + $this->csrfToken = null; $this->sessionStorage->removeToken(); } diff --git a/lib/public/AppFramework/Http/ContentSecurityPolicy.php b/lib/public/AppFramework/Http/ContentSecurityPolicy.php index 082aa0206c7..17844497f94 100644 --- a/lib/public/AppFramework/Http/ContentSecurityPolicy.php +++ b/lib/public/AppFramework/Http/ContentSecurityPolicy.php @@ -24,8 +24,6 @@ namespace OCP\AppFramework\Http; -use OCP\AppFramework\Http; - /** * Class ContentSecurityPolicy is a simple helper which allows applications to * modify the Content-Security-Policy sent by ownCloud. Per default only JavaScript, diff --git a/lib/public/AppFramework/Http/EmptyContentSecurityPolicy.php b/lib/public/AppFramework/Http/EmptyContentSecurityPolicy.php index 4fca1588e7f..ae4ceef1923 100644 --- a/lib/public/AppFramework/Http/EmptyContentSecurityPolicy.php +++ b/lib/public/AppFramework/Http/EmptyContentSecurityPolicy.php @@ -38,6 +38,8 @@ use OCP\AppFramework\Http; class EmptyContentSecurityPolicy { /** @var bool Whether inline JS snippets are allowed */ protected $inlineScriptAllowed = null; + /** @var string Whether JS nonces should be used */ + protected $useJsNonce = null; /** * @var bool Whether eval in JS scripts is allowed * TODO: Disallow per default @@ -74,6 +76,7 @@ class EmptyContentSecurityPolicy { * @param bool $state * @return $this * @since 8.1.0 + * @deprecated 10.0 CSP tokens are now used */ public function allowInlineScript($state = false) { $this->inlineScriptAllowed = $state; @@ -81,6 +84,18 @@ class EmptyContentSecurityPolicy { } /** + * Use the according JS nonce + * + * @param string $nonce + * @return $this + * @since 9.2.0 + */ + public function useJsNonce($nonce) { + $this->useJsNonce = $nonce; + return $this; + } + + /** * Whether eval in JavaScript is allowed or forbidden * @param bool $state * @return $this @@ -323,6 +338,15 @@ class EmptyContentSecurityPolicy { if(!empty($this->allowedScriptDomains) || $this->inlineScriptAllowed || $this->evalScriptAllowed) { $policy .= 'script-src '; + if(is_string($this->useJsNonce)) { + $policy .= '\'nonce-'.base64_encode($this->useJsNonce).'\''; + $allowedScriptDomains = array_flip($this->allowedScriptDomains); + unset($allowedScriptDomains['\'self\'']); + $this->allowedScriptDomains = array_flip($allowedScriptDomains); + if(count($allowedScriptDomains) !== 0) { + $policy .= ' '; + } + } if(is_array($this->allowedScriptDomains)) { $policy .= implode(' ', $this->allowedScriptDomains); } |