diff options
author | Morris Jobke <hey@morrisjobke.de> | 2016-10-25 14:46:00 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-10-25 14:46:00 +0200 |
commit | 89574367bcc57da5eda6d13ffcfd8a12de68ea26 (patch) | |
tree | 8947e9bdc83b55521a4fa52c40e82c6ee7646701 /lib | |
parent | 27ba46c40ed1d365965a1cb79ed7d8a38d759d2c (diff) | |
parent | ee8b8adf7a62fe4b3823cf803f5f37da3bc6a410 (diff) | |
download | nextcloud-server-89574367bcc57da5eda6d13ffcfd8a12de68ea26.tar.gz nextcloud-server-89574367bcc57da5eda6d13ffcfd8a12de68ea26.zip |
Merge pull request #1871 from nextcloud/use-csp-nonces
Use CSP nonces
Diffstat (limited to 'lib')
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | lib/private/AppFramework/DependencyInjection/DIContainer.php | 3 | ||||
-rw-r--r-- | lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php | 29 | ||||
-rw-r--r-- | lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php | 54 | ||||
-rw-r--r-- | lib/private/Security/CSRF/CsrfToken.php | 10 | ||||
-rw-r--r-- | lib/private/Security/CSRF/CsrfTokenManager.php | 13 | ||||
-rw-r--r-- | lib/private/Server.php | 13 | ||||
-rw-r--r-- | lib/public/AppFramework/Http/ContentSecurityPolicy.php | 2 | ||||
-rw-r--r-- | lib/public/AppFramework/Http/EmptyContentSecurityPolicy.php | 24 |
10 files changed, 142 insertions, 8 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index c10f1ca7e5f..0d2a782a867 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -674,6 +674,7 @@ return array( 'OC\\Security\\Bruteforce\\Throttler' => $baseDir . '/lib/private/Security/Bruteforce/Throttler.php', 'OC\\Security\\CSP\\ContentSecurityPolicy' => $baseDir . '/lib/private/Security/CSP/ContentSecurityPolicy.php', 'OC\\Security\\CSP\\ContentSecurityPolicyManager' => $baseDir . '/lib/private/Security/CSP/ContentSecurityPolicyManager.php', + 'OC\\Security\\CSP\\ContentSecurityPolicyNonceManager' => $baseDir . '/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php', 'OC\\Security\\CSRF\\CsrfToken' => $baseDir . '/lib/private/Security/CSRF/CsrfToken.php', 'OC\\Security\\CSRF\\CsrfTokenGenerator' => $baseDir . '/lib/private/Security/CSRF/CsrfTokenGenerator.php', 'OC\\Security\\CSRF\\CsrfTokenManager' => $baseDir . '/lib/private/Security/CSRF/CsrfTokenManager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 4b7020b59b5..43e3fc3de34 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -704,6 +704,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Security\\Bruteforce\\Throttler' => __DIR__ . '/../../..' . '/lib/private/Security/Bruteforce/Throttler.php', 'OC\\Security\\CSP\\ContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/private/Security/CSP/ContentSecurityPolicy.php', 'OC\\Security\\CSP\\ContentSecurityPolicyManager' => __DIR__ . '/../../..' . '/lib/private/Security/CSP/ContentSecurityPolicyManager.php', + 'OC\\Security\\CSP\\ContentSecurityPolicyNonceManager' => __DIR__ . '/../../..' . '/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php', 'OC\\Security\\CSRF\\CsrfToken' => __DIR__ . '/../../..' . '/lib/private/Security/CSRF/CsrfToken.php', 'OC\\Security\\CSRF\\CsrfTokenGenerator' => __DIR__ . '/../../..' . '/lib/private/Security/CSRF/CsrfTokenGenerator.php', 'OC\\Security\\CSRF\\CsrfTokenManager' => __DIR__ . '/../../..' . '/lib/private/Security/CSRF/CsrfTokenManager.php', 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/CSP/ContentSecurityPolicyNonceManager.php b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php new file mode 100644 index 00000000000..0482ea49e5c --- /dev/null +++ b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php @@ -0,0 +1,54 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Security\CSP; + +use OC\Security\CSRF\CsrfTokenManager; + +/** + * @package OC\Security\CSP + */ +class ContentSecurityPolicyNonceManager { + /** @var CsrfTokenManager */ + private $csrfTokenManager; + /** @var string */ + private $nonce = ''; + + /** + * @param CsrfTokenManager $csrfTokenManager + */ + public function __construct(CsrfTokenManager $csrfTokenManager) { + $this->csrfTokenManager = $csrfTokenManager; + } + + /** + * Returns the current CSP nounce + * + * @return string + */ + public function getNonce() { + if($this->nonce === '') { + $this->nonce = base64_encode($this->csrfTokenManager->getToken()->getEncryptedValue()); + } + + return $this->nonce; + } +} 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/private/Server.php b/lib/private/Server.php index 11558118d52..1ccc27802d2 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -73,6 +73,7 @@ use OC\Security\Bruteforce\Throttler; use OC\Security\CertificateManager; use OC\Security\CSP\ContentSecurityPolicyManager; use OC\Security\Crypto; +use OC\Security\CSP\ContentSecurityPolicyNonceManager; use OC\Security\CSRF\CsrfTokenGenerator; use OC\Security\CSRF\CsrfTokenManager; use OC\Security\CSRF\TokenStorage\SessionStorage; @@ -708,6 +709,11 @@ class Server extends ServerContainer implements IServerContainer { $this->registerService('ContentSecurityPolicyManager', function (Server $c) { return new ContentSecurityPolicyManager(); }); + $this->registerService('ContentSecurityPolicyNonceManager', function(Server $c) { + return new ContentSecurityPolicyNonceManager( + $c->getCsrfTokenManager() + ); + }); $this->registerService('ShareManager', function(Server $c) { $config = $c->getConfig(); $factoryClass = $config->getSystemValue('sharing.managerFactory', '\OC\Share20\ProviderFactory'); @@ -1406,6 +1412,13 @@ class Server extends ServerContainer implements IServerContainer { } /** + * @return ContentSecurityPolicyNonceManager + */ + public function getContentSecurityPolicyNonceManager() { + return $this->query('ContentSecurityPolicyNonceManager'); + } + + /** * Not a public API as of 8.2, wait for 9.0 * * @return \OCA\Files_External\Service\BackendService 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); } |