diff options
author | Thomas Müller <thomas.mueller@tmit.eu> | 2016-02-01 10:10:40 +0100 |
---|---|---|
committer | Thomas Müller <thomas.mueller@tmit.eu> | 2016-02-01 10:10:40 +0100 |
commit | 32067ac49b6f891471a6b5672c903a390b4ac26d (patch) | |
tree | f657896888f3cdf59a377fc6ea828f8dea31df11 /lib | |
parent | a025b2865f54a586151f84d5259a41850252c21a (diff) | |
parent | 809ff5ac95080b02d1ba4bba76efab59ff70122b (diff) | |
download | nextcloud-server-32067ac49b6f891471a6b5672c903a390b4ac26d.tar.gz nextcloud-server-32067ac49b6f891471a6b5672c903a390b4ac26d.zip |
Merge pull request #21989 from owncloud/make-csp-modifiable
Add public API to give developers the possibility to adjust the global CSP defaults
Diffstat (limited to 'lib')
-rw-r--r-- | lib/private/appframework/dependencyinjection/dicontainer.php | 10 | ||||
-rw-r--r-- | lib/private/appframework/middleware/security/securitymiddleware.php | 37 | ||||
-rw-r--r-- | lib/private/security/csp/contentsecuritypolicy.php | 199 | ||||
-rw-r--r-- | lib/private/security/csp/contentsecuritypolicymanager.php | 73 | ||||
-rw-r--r-- | lib/private/server.php | 12 | ||||
-rw-r--r-- | lib/public/appframework/controller.php | 2 | ||||
-rw-r--r-- | lib/public/appframework/http/contentsecuritypolicy.php | 335 | ||||
-rw-r--r-- | lib/public/appframework/http/emptycontentsecuritypolicy.php | 387 | ||||
-rw-r--r-- | lib/public/iservercontainer.php | 7 | ||||
-rw-r--r-- | lib/public/security/icontentsecuritypolicymanager.php | 50 |
10 files changed, 783 insertions, 329 deletions
diff --git a/lib/private/appframework/dependencyinjection/dicontainer.php b/lib/private/appframework/dependencyinjection/dicontainer.php index 61a04482431..ff9da88cd81 100644 --- a/lib/private/appframework/dependencyinjection/dicontainer.php +++ b/lib/private/appframework/dependencyinjection/dicontainer.php @@ -32,7 +32,6 @@ namespace OC\AppFramework\DependencyInjection; use OC; use OC\AppFramework\Http; -use OC\AppFramework\Http\Request; use OC\AppFramework\Http\Dispatcher; use OC\AppFramework\Http\Output; use OC\AppFramework\Core\API; @@ -43,8 +42,6 @@ use OC\AppFramework\Middleware\SessionMiddleware; use OC\AppFramework\Utility\SimpleContainer; use OCP\AppFramework\IApi; use OCP\AppFramework\IAppContainer; -use OCP\AppFramework\Middleware; -use OCP\IServerContainer; class DIContainer extends SimpleContainer implements IAppContainer { @@ -255,6 +252,10 @@ class DIContainer extends SimpleContainer implements IAppContainer { return $this->getServer()->getSession(); }); + $this->registerService('OCP\\Security\\IContentSecurityPolicyManager', function($c) { + return $this->getServer()->getContentSecurityPolicyManager(); + }); + $this->registerService('ServerContainer', function ($c) { return $this->getServer(); }); @@ -319,7 +320,8 @@ class DIContainer extends SimpleContainer implements IAppContainer { $app->getServer()->getLogger(), $c['AppName'], $app->isLoggedIn(), - $app->isAdminUser() + $app->isAdminUser(), + $app->getServer()->getContentSecurityPolicyManager() ); }); diff --git a/lib/private/appframework/middleware/security/securitymiddleware.php b/lib/private/appframework/middleware/security/securitymiddleware.php index 4ef043ad50f..f1ba98254f0 100644 --- a/lib/private/appframework/middleware/security/securitymiddleware.php +++ b/lib/private/appframework/middleware/security/securitymiddleware.php @@ -32,6 +32,8 @@ use OC\Appframework\Middleware\Security\Exceptions\CrossSiteRequestForgeryExcept use OC\Appframework\Middleware\Security\Exceptions\NotAdminException; use OC\Appframework\Middleware\Security\Exceptions\NotLoggedInException; use OC\AppFramework\Utility\ControllerMethodReflector; +use OC\Security\CSP\ContentSecurityPolicyManager; +use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Middleware; @@ -52,15 +54,24 @@ use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; * check fails */ class SecurityMiddleware extends Middleware { - + /** @var INavigationManager */ private $navigationManager; + /** @var IRequest */ private $request; + /** @var ControllerMethodReflector */ private $reflector; + /** @var string */ private $appName; + /** @var IURLGenerator */ private $urlGenerator; + /** @var ILogger */ private $logger; + /** @var bool */ private $isLoggedIn; + /** @var bool */ private $isAdminUser; + /** @var ContentSecurityPolicyManager */ + private $contentSecurityPolicyManager; /** * @param IRequest $request @@ -71,6 +82,7 @@ class SecurityMiddleware extends Middleware { * @param string $appName * @param bool $isLoggedIn * @param bool $isAdminUser + * @param ContentSecurityPolicyManager $contentSecurityPolicyManager */ public function __construct(IRequest $request, ControllerMethodReflector $reflector, @@ -79,7 +91,8 @@ class SecurityMiddleware extends Middleware { ILogger $logger, $appName, $isLoggedIn, - $isAdminUser) { + $isAdminUser, + ContentSecurityPolicyManager $contentSecurityPolicyManager) { $this->navigationManager = $navigationManager; $this->request = $request; $this->reflector = $reflector; @@ -88,6 +101,7 @@ class SecurityMiddleware extends Middleware { $this->logger = $logger; $this->isLoggedIn = $isLoggedIn; $this->isAdminUser = $isAdminUser; + $this->contentSecurityPolicyManager = $contentSecurityPolicyManager; } @@ -139,6 +153,25 @@ class SecurityMiddleware extends Middleware { } + /** + * Performs the default CSP modifications that may be injected by other + * applications + * + * @param Controller $controller + * @param string $methodName + * @param Response $response + * @return Response + */ + public function afterController($controller, $methodName, Response $response) { + $policy = !is_null($response->getContentSecurityPolicy()) ? $response->getContentSecurityPolicy() : new ContentSecurityPolicy(); + + $defaultPolicy = $this->contentSecurityPolicyManager->getDefaultPolicy(); + $defaultPolicy = $this->contentSecurityPolicyManager->mergePolicies($defaultPolicy, $policy); + + $response->setContentSecurityPolicy($defaultPolicy); + + return $response; + } /** * If an SecurityException is being caught, ajax requests return a JSON error diff --git a/lib/private/security/csp/contentsecuritypolicy.php b/lib/private/security/csp/contentsecuritypolicy.php new file mode 100644 index 00000000000..25eacfab1d6 --- /dev/null +++ b/lib/private/security/csp/contentsecuritypolicy.php @@ -0,0 +1,199 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OC\Security\CSP; + +/** + * Class ContentSecurityPolicy extends the public class and adds getter and setters. + * This is necessary since we don't want to expose the setters and getters to the + * public API. + * + * @package OC\Security\CSP + */ +class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy { + /** + * @return boolean + */ + public function isInlineScriptAllowed() { + return $this->inlineScriptAllowed; + } + + /** + * @param boolean $inlineScriptAllowed + */ + public function setInlineScriptAllowed($inlineScriptAllowed) { + $this->inlineScriptAllowed = $inlineScriptAllowed; + } + + /** + * @return boolean + */ + public function isEvalScriptAllowed() { + return $this->evalScriptAllowed; + } + + /** + * @param boolean $evalScriptAllowed + */ + public function setEvalScriptAllowed($evalScriptAllowed) { + $this->evalScriptAllowed = $evalScriptAllowed; + } + + /** + * @return array + */ + public function getAllowedScriptDomains() { + return $this->allowedScriptDomains; + } + + /** + * @param array $allowedScriptDomains + */ + public function setAllowedScriptDomains($allowedScriptDomains) { + $this->allowedScriptDomains = $allowedScriptDomains; + } + + /** + * @return boolean + */ + public function isInlineStyleAllowed() { + return $this->inlineStyleAllowed; + } + + /** + * @param boolean $inlineStyleAllowed + */ + public function setInlineStyleAllowed($inlineStyleAllowed) { + $this->inlineStyleAllowed = $inlineStyleAllowed; + } + + /** + * @return array + */ + public function getAllowedStyleDomains() { + return $this->allowedStyleDomains; + } + + /** + * @param array $allowedStyleDomains + */ + public function setAllowedStyleDomains($allowedStyleDomains) { + $this->allowedStyleDomains = $allowedStyleDomains; + } + + /** + * @return array + */ + public function getAllowedImageDomains() { + return $this->allowedImageDomains; + } + + /** + * @param array $allowedImageDomains + */ + public function setAllowedImageDomains($allowedImageDomains) { + $this->allowedImageDomains = $allowedImageDomains; + } + + /** + * @return array + */ + public function getAllowedConnectDomains() { + return $this->allowedConnectDomains; + } + + /** + * @param array $allowedConnectDomains + */ + public function setAllowedConnectDomains($allowedConnectDomains) { + $this->allowedConnectDomains = $allowedConnectDomains; + } + + /** + * @return array + */ + public function getAllowedMediaDomains() { + return $this->allowedMediaDomains; + } + + /** + * @param array $allowedMediaDomains + */ + public function setAllowedMediaDomains($allowedMediaDomains) { + $this->allowedMediaDomains = $allowedMediaDomains; + } + + /** + * @return array + */ + public function getAllowedObjectDomains() { + return $this->allowedObjectDomains; + } + + /** + * @param array $allowedObjectDomains + */ + public function setAllowedObjectDomains($allowedObjectDomains) { + $this->allowedObjectDomains = $allowedObjectDomains; + } + + /** + * @return array + */ + public function getAllowedFrameDomains() { + return $this->allowedFrameDomains; + } + + /** + * @param array $allowedFrameDomains + */ + public function setAllowedFrameDomains($allowedFrameDomains) { + $this->allowedFrameDomains = $allowedFrameDomains; + } + + /** + * @return array + */ + public function getAllowedFontDomains() { + return $this->allowedFontDomains; + } + + /** + * @param array $allowedFontDomains + */ + public function setAllowedFontDomains($allowedFontDomains) { + $this->allowedFontDomains = $allowedFontDomains; + } + + /** + * @return array + */ + public function getAllowedChildSrcDomains() { + return $this->allowedChildSrcDomains; + } + + /** + * @param array $allowedChildSrcDomains + */ + public function setAllowedChildSrcDomains($allowedChildSrcDomains) { + $this->allowedChildSrcDomains = $allowedChildSrcDomains; + } + +} diff --git a/lib/private/security/csp/contentsecuritypolicymanager.php b/lib/private/security/csp/contentsecuritypolicymanager.php new file mode 100644 index 00000000000..760cd36e56b --- /dev/null +++ b/lib/private/security/csp/contentsecuritypolicymanager.php @@ -0,0 +1,73 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Security\CSP; + +use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\EmptyContentSecurityPolicy; +use OCP\Security\IContentSecurityPolicyManager; + +class ContentSecurityPolicyManager implements IContentSecurityPolicyManager { + /** @var ContentSecurityPolicy[] */ + private $policies = []; + + /** {@inheritdoc} */ + public function addDefaultPolicy(EmptyContentSecurityPolicy $policy) { + $this->policies[] = $policy; + } + + /** + * Get the configured default policy. This is not in the public namespace + * as it is only supposed to be used by core itself. + * + * @return ContentSecurityPolicy + */ + public function getDefaultPolicy() { + $defaultPolicy = new \OC\Security\CSP\ContentSecurityPolicy(); + foreach($this->policies as $policy) { + $defaultPolicy = $this->mergePolicies($defaultPolicy, $policy); + } + return $defaultPolicy; + } + + /** + * Merges the first given policy with the second one + * + * @param ContentSecurityPolicy $defaultPolicy + * @param EmptyContentSecurityPolicy $originalPolicy + * @return ContentSecurityPolicy + */ + public function mergePolicies(ContentSecurityPolicy $defaultPolicy, + EmptyContentSecurityPolicy $originalPolicy) { + foreach((object)(array)$originalPolicy as $name => $value) { + $setter = 'set'.ucfirst($name); + if(is_array($value)) { + $getter = 'get'.ucfirst($name); + $currentValues = is_array($defaultPolicy->$getter()) ? $defaultPolicy->$getter() : []; + $defaultPolicy->$setter(array_values(array_unique(array_merge($currentValues, $value)))); + } elseif (is_bool($value)) { + $defaultPolicy->$setter($value); + } + } + + return $defaultPolicy; + } +} diff --git a/lib/private/server.php b/lib/private/server.php index d453a42d3a0..d3dbcba86ba 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -63,6 +63,7 @@ use OC\Lock\NoopLockingProvider; use OC\Mail\Mailer; use OC\Notification\Manager; use OC\Security\CertificateManager; +use OC\Security\CSP\ContentSecurityPolicyManager; use OC\Security\Crypto; use OC\Security\CSRF\CsrfTokenGenerator; use OC\Security\CSRF\CsrfTokenManager; @@ -74,6 +75,7 @@ use OC\Security\TrustedDomainHelper; use OC\Session\CryptoWrapper; use OC\Tagging\TagMapper; use OCP\IServerContainer; +use OCP\Security\IContentSecurityPolicyManager; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -598,6 +600,9 @@ class Server extends ServerContainer implements IServerContainer { $sessionStorage ); }); + $this->registerService('ContentSecurityPolicyManager', function (Server $c) { + return new ContentSecurityPolicyManager(); + }); $this->registerService('ShareManager', function(Server $c) { $config = $c->getConfig(); $factoryClass = $config->getSystemValue('sharing.managerFactory', '\OC\Share20\ProviderFactory'); @@ -1221,6 +1226,13 @@ class Server extends ServerContainer implements IServerContainer { } /** + * @return IContentSecurityPolicyManager + */ + public function getContentSecurityPolicyManager() { + return $this->query('ContentSecurityPolicyManager'); + } + + /** * Not a public API as of 8.2, wait for 9.0 * * @return \OCA\Files_External\Service\BackendService diff --git a/lib/public/appframework/controller.php b/lib/public/appframework/controller.php index 973c9044684..c3744683300 100644 --- a/lib/public/appframework/controller.php +++ b/lib/public/appframework/controller.php @@ -72,7 +72,7 @@ abstract class Controller { * @since 6.0.0 - parameter $appName was added in 7.0.0 - parameter $app was removed in 7.0.0 */ public function __construct($appName, - IRequest $request){ + IRequest $request) { $this->appName = $appName; $this->request = $request; diff --git a/lib/public/appframework/http/contentsecuritypolicy.php b/lib/public/appframework/http/contentsecuritypolicy.php index 35da4f05e80..7762ca809a2 100644 --- a/lib/public/appframework/http/contentsecuritypolicy.php +++ b/lib/public/appframework/http/contentsecuritypolicy.php @@ -38,17 +38,17 @@ use OCP\AppFramework\Http; * @package OCP\AppFramework\Http * @since 8.1.0 */ -class ContentSecurityPolicy { +class ContentSecurityPolicy extends EmptyContentSecurityPolicy { /** @var bool Whether inline JS snippets are allowed */ - private $inlineScriptAllowed = false; + protected $inlineScriptAllowed = false; /** * @var bool Whether eval in JS scripts is allowed * TODO: Disallow per default * @link https://github.com/owncloud/core/issues/11925 */ - private $evalScriptAllowed = true; + protected $evalScriptAllowed = true; /** @var array Domains from which scripts can get loaded */ - private $allowedScriptDomains = [ + protected $allowedScriptDomains = [ '\'self\'', ]; /** @@ -56,342 +56,33 @@ class ContentSecurityPolicy { * TODO: Disallow per default * @link https://github.com/owncloud/core/issues/13458 */ - private $inlineStyleAllowed = true; + protected $inlineStyleAllowed = true; /** @var array Domains from which CSS can get loaded */ - private $allowedStyleDomains = [ + protected $allowedStyleDomains = [ '\'self\'', ]; /** @var array Domains from which images can get loaded */ - private $allowedImageDomains = [ + protected $allowedImageDomains = [ '\'self\'', 'data:', 'blob:', ]; /** @var array Domains to which connections can be done */ - private $allowedConnectDomains = [ + protected $allowedConnectDomains = [ '\'self\'', ]; /** @var array Domains from which media elements can be loaded */ - private $allowedMediaDomains = [ + protected $allowedMediaDomains = [ '\'self\'', ]; /** @var array Domains from which object elements can be loaded */ - private $allowedObjectDomains = []; + protected $allowedObjectDomains = []; /** @var array Domains from which iframes can be loaded */ - private $allowedFrameDomains = []; + protected $allowedFrameDomains = []; /** @var array Domains from which fonts can be loaded */ - private $allowedFontDomains = [ + protected $allowedFontDomains = [ '\'self\'', ]; /** @var array Domains from which web-workers and nested browsing content can load elements */ - private $allowedChildSrcDomains = []; - - /** - * Whether inline JavaScript snippets are allowed or forbidden - * @param bool $state - * @return $this - * @since 8.1.0 - */ - public function allowInlineScript($state = false) { - $this->inlineScriptAllowed = $state; - return $this; - } - - /** - * Whether eval in JavaScript is allowed or forbidden - * @param bool $state - * @return $this - * @since 8.1.0 - */ - public function allowEvalScript($state = true) { - $this->evalScriptAllowed = $state; - return $this; - } - - /** - * Allows to execute JavaScript files from a specific domain. Use * to - * allow JavaScript from all domains. - * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. - * @return $this - * @since 8.1.0 - */ - public function addAllowedScriptDomain($domain) { - $this->allowedScriptDomains[] = $domain; - return $this; - } - - /** - * Remove the specified allowed script domain from the allowed domains. - * - * @param string $domain - * @return $this - * @since 8.1.0 - */ - public function disallowScriptDomain($domain) { - $this->allowedScriptDomains = array_diff($this->allowedScriptDomains, [$domain]); - return $this; - } - - /** - * Whether inline CSS snippets are allowed or forbidden - * @param bool $state - * @return $this - * @since 8.1.0 - */ - public function allowInlineStyle($state = true) { - $this->inlineStyleAllowed = $state; - return $this; - } - - /** - * Allows to execute CSS files from a specific domain. Use * to allow - * CSS from all domains. - * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. - * @return $this - * @since 8.1.0 - */ - public function addAllowedStyleDomain($domain) { - $this->allowedStyleDomains[] = $domain; - return $this; - } - - /** - * Remove the specified allowed style domain from the allowed domains. - * - * @param string $domain - * @return $this - * @since 8.1.0 - */ - public function disallowStyleDomain($domain) { - $this->allowedStyleDomains = array_diff($this->allowedStyleDomains, [$domain]); - return $this; - } - - /** - * Allows using fonts from a specific domain. Use * to allow - * fonts from all domains. - * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. - * @return $this - * @since 8.1.0 - */ - public function addAllowedFontDomain($domain) { - $this->allowedFontDomains[] = $domain; - return $this; - } - - /** - * Remove the specified allowed font domain from the allowed domains. - * - * @param string $domain - * @return $this - * @since 8.1.0 - */ - public function disallowFontDomain($domain) { - $this->allowedFontDomains = array_diff($this->allowedFontDomains, [$domain]); - return $this; - } - - /** - * Allows embedding images from a specific domain. Use * to allow - * images from all domains. - * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. - * @return $this - * @since 8.1.0 - */ - public function addAllowedImageDomain($domain) { - $this->allowedImageDomains[] = $domain; - return $this; - } - - /** - * Remove the specified allowed image domain from the allowed domains. - * - * @param string $domain - * @return $this - * @since 8.1.0 - */ - public function disallowImageDomain($domain) { - $this->allowedImageDomains = array_diff($this->allowedImageDomains, [$domain]); - return $this; - } - - /** - * To which remote domains the JS connect to. - * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. - * @return $this - * @since 8.1.0 - */ - public function addAllowedConnectDomain($domain) { - $this->allowedConnectDomains[] = $domain; - return $this; - } - - /** - * Remove the specified allowed connect domain from the allowed domains. - * - * @param string $domain - * @return $this - * @since 8.1.0 - */ - public function disallowConnectDomain($domain) { - $this->allowedConnectDomains = array_diff($this->allowedConnectDomains, [$domain]); - return $this; - } - - /** - * From which domains media elements can be embedded. - * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. - * @return $this - * @since 8.1.0 - */ - public function addAllowedMediaDomain($domain) { - $this->allowedMediaDomains[] = $domain; - return $this; - } - - /** - * Remove the specified allowed media domain from the allowed domains. - * - * @param string $domain - * @return $this - * @since 8.1.0 - */ - public function disallowMediaDomain($domain) { - $this->allowedMediaDomains = array_diff($this->allowedMediaDomains, [$domain]); - return $this; - } - - /** - * From which domains objects such as <object>, <embed> or <applet> are executed - * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. - * @return $this - * @since 8.1.0 - */ - public function addAllowedObjectDomain($domain) { - $this->allowedObjectDomains[] = $domain; - return $this; - } - - /** - * Remove the specified allowed object domain from the allowed domains. - * - * @param string $domain - * @return $this - * @since 8.1.0 - */ - public function disallowObjectDomain($domain) { - $this->allowedObjectDomains = array_diff($this->allowedObjectDomains, [$domain]); - return $this; - } - - /** - * Which domains can be embedded in an iframe - * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. - * @return $this - * @since 8.1.0 - */ - public function addAllowedFrameDomain($domain) { - $this->allowedFrameDomains[] = $domain; - return $this; - } - - /** - * Remove the specified allowed frame domain from the allowed domains. - * - * @param string $domain - * @return $this - * @since 8.1.0 - */ - public function disallowFrameDomain($domain) { - $this->allowedFrameDomains = array_diff($this->allowedFrameDomains, [$domain]); - return $this; - } - - /** - * Domains from which web-workers and nested browsing content can load elements - * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. - * @return $this - * @since 8.1.0 - */ - public function addAllowedChildSrcDomain($domain) { - $this->allowedChildSrcDomains[] = $domain; - return $this; - } - - /** - * Remove the specified allowed child src domain from the allowed domains. - * - * @param string $domain - * @return $this - * @since 8.1.0 - */ - public function disallowChildSrcDomain($domain) { - $this->allowedChildSrcDomains = array_diff($this->allowedChildSrcDomains, [$domain]); - return $this; - } - - /** - * Get the generated Content-Security-Policy as a string - * @return string - * @since 8.1.0 - */ - public function buildPolicy() { - $policy = "default-src 'none';"; - - if(!empty($this->allowedScriptDomains)) { - $policy .= 'script-src ' . implode(' ', $this->allowedScriptDomains); - if($this->inlineScriptAllowed) { - $policy .= ' \'unsafe-inline\''; - } - if($this->evalScriptAllowed) { - $policy .= ' \'unsafe-eval\''; - } - $policy .= ';'; - } - - if(!empty($this->allowedStyleDomains)) { - $policy .= 'style-src ' . implode(' ', $this->allowedStyleDomains); - if($this->inlineStyleAllowed) { - $policy .= ' \'unsafe-inline\''; - } - $policy .= ';'; - } - - if(!empty($this->allowedImageDomains)) { - $policy .= 'img-src ' . implode(' ', $this->allowedImageDomains); - $policy .= ';'; - } - - if(!empty($this->allowedFontDomains)) { - $policy .= 'font-src ' . implode(' ', $this->allowedFontDomains); - $policy .= ';'; - } - - if(!empty($this->allowedConnectDomains)) { - $policy .= 'connect-src ' . implode(' ', $this->allowedConnectDomains); - $policy .= ';'; - } - - if(!empty($this->allowedMediaDomains)) { - $policy .= 'media-src ' . implode(' ', $this->allowedMediaDomains); - $policy .= ';'; - } - - if(!empty($this->allowedObjectDomains)) { - $policy .= 'object-src ' . implode(' ', $this->allowedObjectDomains); - $policy .= ';'; - } - - if(!empty($this->allowedFrameDomains)) { - $policy .= 'frame-src ' . implode(' ', $this->allowedFrameDomains); - $policy .= ';'; - } - - if(!empty($this->allowedChildSrcDomains)) { - $policy .= 'child-src ' . implode(' ', $this->allowedChildSrcDomains); - $policy .= ';'; - } - - return rtrim($policy, ';'); - } + protected $allowedChildSrcDomains = []; } diff --git a/lib/public/appframework/http/emptycontentsecuritypolicy.php b/lib/public/appframework/http/emptycontentsecuritypolicy.php new file mode 100644 index 00000000000..33860dcdb0f --- /dev/null +++ b/lib/public/appframework/http/emptycontentsecuritypolicy.php @@ -0,0 +1,387 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author sualko <klaus@jsxc.org> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; + +/** + * Class EmptyContentSecurityPolicy is a simple helper which allows applications + * to modify the Content-Security-Policy sent by ownCloud. Per default the policy + * is forbidding everything. + * + * As alternative with sane exemptions look at ContentSecurityPolicy + * + * @see \OCP\AppFramework\Http\ContentSecurityPolicy + * @package OCP\AppFramework\Http + * @since 9.0.0 + */ +class EmptyContentSecurityPolicy { + /** @var bool Whether inline JS snippets are allowed */ + protected $inlineScriptAllowed = null; + /** + * @var bool Whether eval in JS scripts is allowed + * TODO: Disallow per default + * @link https://github.com/owncloud/core/issues/11925 + */ + protected $evalScriptAllowed = null; + /** @var array Domains from which scripts can get loaded */ + protected $allowedScriptDomains = null; + /** + * @var bool Whether inline CSS is allowed + * TODO: Disallow per default + * @link https://github.com/owncloud/core/issues/13458 + */ + protected $inlineStyleAllowed = null; + /** @var array Domains from which CSS can get loaded */ + protected $allowedStyleDomains = null; + /** @var array Domains from which images can get loaded */ + protected $allowedImageDomains = null; + /** @var array Domains to which connections can be done */ + protected $allowedConnectDomains = null; + /** @var array Domains from which media elements can be loaded */ + protected $allowedMediaDomains = null; + /** @var array Domains from which object elements can be loaded */ + protected $allowedObjectDomains = null; + /** @var array Domains from which iframes can be loaded */ + protected $allowedFrameDomains = null; + /** @var array Domains from which fonts can be loaded */ + protected $allowedFontDomains = null; + /** @var array Domains from which web-workers and nested browsing content can load elements */ + protected $allowedChildSrcDomains = null; + + /** + * Whether inline JavaScript snippets are allowed or forbidden + * @param bool $state + * @return $this + * @since 8.1.0 + */ + public function allowInlineScript($state = false) { + $this->inlineScriptAllowed = $state; + return $this; + } + + /** + * Whether eval in JavaScript is allowed or forbidden + * @param bool $state + * @return $this + * @since 8.1.0 + */ + public function allowEvalScript($state = true) { + $this->evalScriptAllowed = $state; + return $this; + } + + /** + * Allows to execute JavaScript files from a specific domain. Use * to + * allow JavaScript from all domains. + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedScriptDomain($domain) { + $this->allowedScriptDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed script domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowScriptDomain($domain) { + $this->allowedScriptDomains = array_diff($this->allowedScriptDomains, [$domain]); + return $this; + } + + /** + * Whether inline CSS snippets are allowed or forbidden + * @param bool $state + * @return $this + * @since 8.1.0 + */ + public function allowInlineStyle($state = true) { + $this->inlineStyleAllowed = $state; + return $this; + } + + /** + * Allows to execute CSS files from a specific domain. Use * to allow + * CSS from all domains. + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedStyleDomain($domain) { + $this->allowedStyleDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed style domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowStyleDomain($domain) { + $this->allowedStyleDomains = array_diff($this->allowedStyleDomains, [$domain]); + return $this; + } + + /** + * Allows using fonts from a specific domain. Use * to allow + * fonts from all domains. + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedFontDomain($domain) { + $this->allowedFontDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed font domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowFontDomain($domain) { + $this->allowedFontDomains = array_diff($this->allowedFontDomains, [$domain]); + return $this; + } + + /** + * Allows embedding images from a specific domain. Use * to allow + * images from all domains. + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedImageDomain($domain) { + $this->allowedImageDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed image domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowImageDomain($domain) { + $this->allowedImageDomains = array_diff($this->allowedImageDomains, [$domain]); + return $this; + } + + /** + * To which remote domains the JS connect to. + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedConnectDomain($domain) { + $this->allowedConnectDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed connect domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowConnectDomain($domain) { + $this->allowedConnectDomains = array_diff($this->allowedConnectDomains, [$domain]); + return $this; + } + + /** + * From which domains media elements can be embedded. + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedMediaDomain($domain) { + $this->allowedMediaDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed media domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowMediaDomain($domain) { + $this->allowedMediaDomains = array_diff($this->allowedMediaDomains, [$domain]); + return $this; + } + + /** + * From which domains objects such as <object>, <embed> or <applet> are executed + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedObjectDomain($domain) { + $this->allowedObjectDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed object domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowObjectDomain($domain) { + $this->allowedObjectDomains = array_diff($this->allowedObjectDomains, [$domain]); + return $this; + } + + /** + * Which domains can be embedded in an iframe + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedFrameDomain($domain) { + $this->allowedFrameDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed frame domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowFrameDomain($domain) { + $this->allowedFrameDomains = array_diff($this->allowedFrameDomains, [$domain]); + return $this; + } + + /** + * Domains from which web-workers and nested browsing content can load elements + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedChildSrcDomain($domain) { + $this->allowedChildSrcDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed child src domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowChildSrcDomain($domain) { + $this->allowedChildSrcDomains = array_diff($this->allowedChildSrcDomains, [$domain]); + return $this; + } + + /** + * Get the generated Content-Security-Policy as a string + * @return string + * @since 8.1.0 + */ + public function buildPolicy() { + $policy = "default-src 'none';"; + + if(!empty($this->allowedScriptDomains) || $this->inlineScriptAllowed || $this->evalScriptAllowed) { + $policy .= 'script-src '; + if(is_array($this->allowedScriptDomains)) { + $policy .= implode(' ', $this->allowedScriptDomains); + } + if($this->inlineScriptAllowed) { + $policy .= ' \'unsafe-inline\''; + } + if($this->evalScriptAllowed) { + $policy .= ' \'unsafe-eval\''; + } + $policy .= ';'; + } + + if(!empty($this->allowedStyleDomains) || $this->inlineStyleAllowed) { + $policy .= 'style-src '; + if(is_array($this->allowedStyleDomains)) { + $policy .= implode(' ', $this->allowedStyleDomains); + } + if($this->inlineStyleAllowed) { + $policy .= ' \'unsafe-inline\''; + } + $policy .= ';'; + } + + if(!empty($this->allowedImageDomains)) { + $policy .= 'img-src ' . implode(' ', $this->allowedImageDomains); + $policy .= ';'; + } + + if(!empty($this->allowedFontDomains)) { + $policy .= 'font-src ' . implode(' ', $this->allowedFontDomains); + $policy .= ';'; + } + + if(!empty($this->allowedConnectDomains)) { + $policy .= 'connect-src ' . implode(' ', $this->allowedConnectDomains); + $policy .= ';'; + } + + if(!empty($this->allowedMediaDomains)) { + $policy .= 'media-src ' . implode(' ', $this->allowedMediaDomains); + $policy .= ';'; + } + + if(!empty($this->allowedObjectDomains)) { + $policy .= 'object-src ' . implode(' ', $this->allowedObjectDomains); + $policy .= ';'; + } + + if(!empty($this->allowedFrameDomains)) { + $policy .= 'frame-src ' . implode(' ', $this->allowedFrameDomains); + $policy .= ';'; + } + + if(!empty($this->allowedChildSrcDomains)) { + $policy .= 'child-src ' . implode(' ', $this->allowedChildSrcDomains); + $policy .= ';'; + } + + return rtrim($policy, ';'); + } +} diff --git a/lib/public/iservercontainer.php b/lib/public/iservercontainer.php index ce1364cc4ea..de48daeef88 100644 --- a/lib/public/iservercontainer.php +++ b/lib/public/iservercontainer.php @@ -42,6 +42,7 @@ // use OCP namespace for all classes that are considered public. // This means that they should be used by apps instead of the internal ownCloud classes namespace OCP; +use OCP\Security\IContentSecurityPolicyManager; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -512,4 +513,10 @@ interface IServerContainer { * @since 9.0.0 */ public function getShareManager(); + + /** + * @return IContentSecurityPolicyManager + * @since 9.0.0 + */ + public function getContentSecurityPolicyManager(); } diff --git a/lib/public/security/icontentsecuritypolicymanager.php b/lib/public/security/icontentsecuritypolicymanager.php new file mode 100644 index 00000000000..10f1efe995f --- /dev/null +++ b/lib/public/security/icontentsecuritypolicymanager.php @@ -0,0 +1,50 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCP\Security; +use OCP\AppFramework\Http\EmptyContentSecurityPolicy; + +/** + * Used for Content Security Policy manipulations + * + * @package OCP\Security + * @since 9.0.0 + */ +interface IContentSecurityPolicyManager { + /** + * Allows to inject something into the default content policy. This is for + * example useful when you're injecting Javascript code into a view belonging + * to another controller and cannot modify its Content-Security-Policy itself. + * Note that the adjustment is only applied to applications that use AppFramework + * controllers. + * + * To use this from your `app.php` use `\OC::$server->getContentSecurityPolicyManager()->addDefaultPolicy($policy)`, + * $policy has to be of type `\OCP\AppFramework\Http\ContentSecurityPolicy`. + * + * WARNING: Using this API incorrectly may make the instance more insecure. + * Do think twice before adding whitelisting resources. Please do also note + * that it is not possible to use the `disallowXYZ` functions. + * + * @param EmptyContentSecurityPolicy $policy + * @since 9.0.0 + */ + public function addDefaultPolicy(EmptyContentSecurityPolicy $policy); +} |