Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/theming/appinfo/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
'script',
[
'src' => $linkToJs,
'nonce' => \OC::$server->getContentSecurityPolicyNonceManager()->getNonce()
], ''
);

Expand Down
2 changes: 1 addition & 1 deletion core/templates/layout.base.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<link rel="stylesheet" href="<?php print_unescaped($cssfile); ?>" media="print">
<?php endforeach; ?>
<?php foreach ($_['jsfiles'] as $jsfile): ?>
<script src="<?php print_unescaped($jsfile); ?>"></script>
<script nonce="<?php p(\OC::$server->getContentSecurityPolicyNonceManager()->getNonce()) ?>" src="<?php print_unescaped($jsfile); ?>"></script>
<?php endforeach; ?>
<?php print_unescaped($_['headers']); ?>
</head>
Expand Down
2 changes: 1 addition & 1 deletion core/templates/layout.guest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<link rel="stylesheet" href="<?php print_unescaped($cssfile); ?>" media="print">
<?php endforeach; ?>
<?php foreach($_['jsfiles'] as $jsfile): ?>
<script src="<?php print_unescaped($jsfile); ?>"></script>
<script nonce="<?php p(\OC::$server->getContentSecurityPolicyNonceManager()->getNonce()) ?>" src="<?php print_unescaped($jsfile); ?>"></script>
<?php endforeach; ?>
<?php print_unescaped($_['headers']); ?>
</head>
Expand Down
2 changes: 1 addition & 1 deletion core/templates/layout.user.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<link rel="stylesheet" href="<?php print_unescaped($cssfile); ?>" media="print">
<?php endforeach; ?>
<?php foreach($_['jsfiles'] as $jsfile): ?>
<script src="<?php print_unescaped($jsfile); ?>"></script>
<script nonce="<?php p(\OC::$server->getContentSecurityPolicyNonceManager()->getNonce()) ?>" src="<?php print_unescaped($jsfile); ?>"></script>
<?php endforeach; ?>
<?php print_unescaped($_['headers']); ?>
</head>
Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,7 @@
'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',
Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,8 @@ public function __construct($appName, $urlParams = array()){
$c['AppName'],
$app->isLoggedIn(),
$app->isAdminUser(),
$app->getServer()->getContentSecurityPolicyManager()
$app->getServer()->getContentSecurityPolicyManager(),
$app->getServer()->getCsrfTokenManager()
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
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;
Expand Down Expand Up @@ -77,6 +78,8 @@ class SecurityMiddleware extends Middleware {
private $isAdminUser;
/** @var ContentSecurityPolicyManager */
private $contentSecurityPolicyManager;
/** @var CsrfTokenManager */
private $csrfTokenManager;

/**
* @param IRequest $request
Expand All @@ -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,
Expand All @@ -97,7 +101,8 @@ public function __construct(IRequest $request,
$appName,
$isLoggedIn,
$isAdminUser,
ContentSecurityPolicyManager $contentSecurityPolicyManager) {
ContentSecurityPolicyManager $contentSecurityPolicyManager,
CsrfTokenManager $csrfTokenManager) {
$this->navigationManager = $navigationManager;
$this->request = $request;
$this->reflector = $reflector;
Expand All @@ -107,6 +112,7 @@ public function __construct(IRequest $request,
$this->isLoggedIn = $isLoggedIn;
$this->isAdminUser = $isAdminUser;
$this->contentSecurityPolicyManager = $contentSecurityPolicyManager;
$this->csrfTokenManager = $csrfTokenManager;
}


Expand Down Expand Up @@ -171,6 +177,23 @@ public function beforeController($controller, $methodName) {

}

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
Expand All @@ -190,6 +213,10 @@ public function afterController($controller, $methodName, Response $response) {
$defaultPolicy = $this->contentSecurityPolicyManager->getDefaultPolicy();
$defaultPolicy = $this->contentSecurityPolicyManager->mergePolicies($defaultPolicy, $policy);

if($this->browserSupportsCspV3()) {
$defaultPolicy->useJsNonce($this->csrfTokenManager->getToken()->getEncryptedValue());
}

$response->setContentSecurityPolicy($defaultPolicy);

return $response;
Expand Down
54 changes: 54 additions & 0 deletions lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
10 changes: 8 additions & 2 deletions lib/private/Security/CSRF/CsrfToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
class CsrfToken {
/** @var string */
private $value;
/** @var string */
private $encryptedValue = '';

/**
* @param string $value Value of the token. Can be encrypted or not encrypted.
Expand All @@ -48,8 +50,12 @@ public function __construct($value) {
* @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;
}

/**
Expand Down
13 changes: 11 additions & 2 deletions lib/private/Security/CSRF/CsrfTokenManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class CsrfTokenManager {
private $tokenGenerator;
/** @var SessionStorage */
private $sessionStorage;
/** @var CsrfToken|null */
private $csrfToken = null;

/**
* @param CsrfTokenGenerator $tokenGenerator
Expand All @@ -51,14 +53,19 @@ public function __construct(CsrfTokenGenerator $tokenGenerator,
* @return CsrfToken
*/
public function getToken() {
if(!is_null($this->csrfToken)) {
return $this->csrfToken;
}

if($this->sessionStorage->hasToken()) {
$value = $this->sessionStorage->getToken();
} else {
$value = $this->tokenGenerator->generateToken();
$this->sessionStorage->setToken($value);
}

return new CsrfToken($value);
$this->csrfToken = new CsrfToken($value);
return $this->csrfToken;
}

/**
Expand All @@ -69,13 +76,15 @@ public function getToken() {
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();
}

Expand Down
13 changes: 13 additions & 0 deletions lib/private/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
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;
Expand Down Expand Up @@ -708,6 +709,11 @@ public function __construct($webRoot, \OC\Config $config) {
$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');
Expand Down Expand Up @@ -1405,6 +1411,13 @@ public function getContentSecurityPolicyManager() {
return $this->query('ContentSecurityPolicyManager');
}

/**
* @return ContentSecurityPolicyNonceManager
*/
public function getContentSecurityPolicyNonceManager() {
return $this->query('ContentSecurityPolicyNonceManager');
}

/**
* Not a public API as of 8.2, wait for 9.0
*
Expand Down
2 changes: 0 additions & 2 deletions lib/public/AppFramework/Http/ContentSecurityPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions lib/public/AppFramework/Http/EmptyContentSecurityPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
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
Expand Down Expand Up @@ -74,12 +76,25 @@ 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;
return $this;
}

/**
* 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
Expand Down Expand Up @@ -323,6 +338,15 @@ public function buildPolicy() {

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);
}
Expand Down
24 changes: 24 additions & 0 deletions tests/lib/AppFramework/Http/EmptyContentSecurityPolicyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -427,4 +427,28 @@ public function testGetPolicyDisallowChildSrcDomainMultipleStakes() {
$this->contentSecurityPolicy->disallowChildSrcDomain('www.owncloud.org')->disallowChildSrcDomain('www.owncloud.com');
$this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy());
}

public function testGetPolicyWithJsNonceAndScriptDomains() {
$expectedPolicy = "default-src 'none';script-src 'nonce-TXlKc05vbmNl' www.nextcloud.com www.nextcloud.org";

$this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com');
$this->contentSecurityPolicy->useJsNonce('MyJsNonce');
$this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.org');
$this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy());
}

public function testGetPolicyWithJsNonceAndSelfScriptDomain() {
$expectedPolicy = "default-src 'none';script-src 'nonce-TXlKc05vbmNl'";

$this->contentSecurityPolicy->useJsNonce('MyJsNonce');
$this->contentSecurityPolicy->addAllowedScriptDomain("'self'");
$this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy());
}

public function testGetPolicyWithoutJsNonceAndSelfScriptDomain() {
$expectedPolicy = "default-src 'none';script-src 'self'";

$this->contentSecurityPolicy->addAllowedScriptDomain("'self'");
$this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy());
}
}
Loading