diff options
author | Clark Tomlinson <fallen013@gmail.com> | 2015-02-18 10:27:29 -0500 |
---|---|---|
committer | Clark Tomlinson <fallen013@gmail.com> | 2015-02-18 10:27:29 -0500 |
commit | 8d09cc3b91a9689a6c95e06c8002288bdd8d5bbf (patch) | |
tree | 81e09b101401476c2de80460a994a34ff26b75d8 /lib | |
parent | 84cc90a0ee81d32001ccaa38795cbcf4343ac2f0 (diff) | |
parent | a9d1a0144018e60ba2728708bf965b4d9855920b (diff) | |
download | nextcloud-server-8d09cc3b91a9689a6c95e06c8002288bdd8d5bbf.tar.gz nextcloud-server-8d09cc3b91a9689a6c95e06c8002288bdd8d5bbf.zip |
Merge pull request #13989 from owncloud/enhancment/security/11857
Allow AppFramework applications to specify a custom CSP header
Diffstat (limited to 'lib')
-rw-r--r-- | lib/private/response.php | 15 | ||||
-rw-r--r-- | lib/public/appframework/http/contentsecuritypolicy.php | 241 | ||||
-rw-r--r-- | lib/public/appframework/http/response.php | 30 |
3 files changed, 279 insertions, 7 deletions
diff --git a/lib/private/response.php b/lib/private/response.php index 9be5d75c314..600b702810c 100644 --- a/lib/private/response.php +++ b/lib/private/response.php @@ -189,7 +189,7 @@ class OC_Response { } } - /* + /** * This function adds some security related headers to all requests served via base.php * The implementation of this function has to happen here to ensure that all third-party * components (e.g. SabreDAV) also benefit from this headers. @@ -204,17 +204,20 @@ class OC_Response { header('X-Frame-Options: Sameorigin'); // Disallow iFraming from other domains } - // Content Security Policy - // If you change the standard policy, please also change it in config.sample.php - $policy = OC_Config::getValue('custom_csp_policy', - 'default-src \'self\'; ' + /** + * FIXME: Content Security Policy for legacy ownCloud components. This + * can be removed once \OCP\AppFramework\Http\Response from the AppFramework + * is used everywhere. + * @see \OCP\AppFramework\Http\Response::getHeaders + */ + $policy = 'default-src \'self\'; ' . 'script-src \'self\' \'unsafe-eval\'; ' . 'style-src \'self\' \'unsafe-inline\'; ' . 'frame-src *; ' . 'img-src *; ' . 'font-src \'self\' data:; ' . 'media-src *; ' - . 'connect-src *'); + . 'connect-src *'; header('Content-Security-Policy:' . $policy); // https://developers.google.com/webmasters/control-crawl-index/docs/robots_meta_tag diff --git a/lib/public/appframework/http/contentsecuritypolicy.php b/lib/public/appframework/http/contentsecuritypolicy.php new file mode 100644 index 00000000000..cb9a241d8af --- /dev/null +++ b/lib/public/appframework/http/contentsecuritypolicy.php @@ -0,0 +1,241 @@ +<?php +/** + * Copyright (c) 2015 Lukas Reschke lukas@owncloud.com + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +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, + * stylesheets, images, fonts, media and connections from the same domain + * ('self') are allowed. + * + * Even if a value gets modified above defaults will still get appended. Please + * notice that ownCloud ships already with sensible defaults and those policies + * should require no modification at all for most use-cases. + * + * @package OCP\AppFramework\Http + */ +class ContentSecurityPolicy { + /** @var bool Whether inline JS snippets are allowed */ + private $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; + /** @var array Domains from which scripts can get loaded */ + private $allowedScriptDomains = [ + '\'self\'', + ]; + /** + * @var bool Whether inline CSS is allowed + * TODO: Disallow per default + * @link https://github.com/owncloud/core/issues/13458 + */ + private $inlineStyleAllowed = true; + /** @var array Domains from which CSS can get loaded */ + private $allowedStyleDomains = [ + '\'self\'', + ]; + /** @var array Domains from which images can get loaded */ + private $allowedImageDomains = [ + '\'self\'', + ]; + /** @var array Domains to which connections can be done */ + private $allowedConnectDomains = [ + '\'self\'', + ]; + /** @var array Domains from which media elements can be loaded */ + private $allowedMediaDomains = [ + '\'self\'', + ]; + /** @var array Domains from which object elements can be loaded */ + private $allowedObjectDomains = []; + /** @var array Domains from which iframes can be loaded */ + private $allowedFrameDomains = []; + /** @var array Domains from which fonts can be loaded */ + private $allowedFontDomains = [ + '\'self\'', + ]; + + /** + * Whether inline JavaScript snippets are allowed or forbidden + * @param bool $state + * @return $this + */ + public function allowInlineScript($state = false) { + $this->inlineScriptAllowed = $state; + return $this; + } + + /** + * Whether eval in JavaScript is allowed or forbidden + * @param bool $state + * @return $this + */ + 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 + */ + public function addAllowedScriptDomain($domain) { + $this->allowedScriptDomains[] = $domain; + return $this; + } + + /** + * Whether inline CSS snippets are allowed or forbidden + * @param bool $state + * @return $this + */ + 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 + */ + public function addAllowedStyleDomain($domain) { + $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 + */ + public function addAllowedFontDomain($domain) { + $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 + */ + public function addAllowedImageDomain($domain) { + $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 + */ + public function addAllowedConnectDomain($domain) { + $this->allowedConnectDomains[] = $domain; + return $this; + } + + /** + * From whoch domains media elements can be embedded. + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + */ + public function addAllowedMediaDomain($domain) { + $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 + */ + public function addAllowedObjectDomain($domain) { + $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 + */ + public function addAllowedFrameDomain($domain) { + $this->allowedFrameDomains[] = $domain; + return $this; + } + + /** + * Get the generated Content-Security-Policy as a string + * @return string + */ + 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 .= ';'; + } + + return rtrim($policy, ';'); + } +} diff --git a/lib/public/appframework/http/response.php b/lib/public/appframework/http/response.php index 67e72cff6d9..751c48b4ca9 100644 --- a/lib/public/appframework/http/response.php +++ b/lib/public/appframework/http/response.php @@ -72,6 +72,9 @@ class Response { */ private $ETag; + /** @var ContentSecurityPolicy|null Used Content-Security-Policy */ + private $contentSecurityPolicy = null; + /** * Caches the response @@ -186,13 +189,19 @@ class Response { * @return array the headers */ public function getHeaders() { - $mergeWith = array(); + $mergeWith = []; if($this->lastModified) { $mergeWith['Last-Modified'] = $this->lastModified->format(\DateTime::RFC2822); } + // Build Content-Security-Policy and use default if none has been specified + if(is_null($this->contentSecurityPolicy)) { + $this->setContentSecurityPolicy(new ContentSecurityPolicy()); + } + $this->headers['Content-Security-Policy'] = $this->contentSecurityPolicy->buildPolicy(); + if($this->ETag) { $mergeWith['ETag'] = '"' . $this->ETag . '"'; } @@ -221,6 +230,25 @@ class Response { return $this; } + /** + * Set a Content-Security-Policy + * @param ContentSecurityPolicy $csp Policy to set for the response object + * @return $this + */ + public function setContentSecurityPolicy(ContentSecurityPolicy $csp) { + $this->contentSecurityPolicy = $csp; + return $this; + } + + /** + * Get the currently used Content-Security-Policy + * @return ContentSecurityPolicy|null Used Content-Security-Policy or null if + * none specified. + */ + public function getContentSecurityPolicy() { + return $this->contentSecurityPolicy; + } + /** * Get response status |