summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorClark Tomlinson <fallen013@gmail.com>2015-02-18 10:27:29 -0500
committerClark Tomlinson <fallen013@gmail.com>2015-02-18 10:27:29 -0500
commit8d09cc3b91a9689a6c95e06c8002288bdd8d5bbf (patch)
tree81e09b101401476c2de80460a994a34ff26b75d8 /lib
parent84cc90a0ee81d32001ccaa38795cbcf4343ac2f0 (diff)
parenta9d1a0144018e60ba2728708bf965b4d9855920b (diff)
downloadnextcloud-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.php15
-rw-r--r--lib/public/appframework/http/contentsecuritypolicy.php241
-rw-r--r--lib/public/appframework/http/response.php30
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