diff options
author | Roeland Jago Douma <rullzer@users.noreply.github.com> | 2019-08-11 10:16:58 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-11 10:16:58 +0200 |
commit | 773ce9e58f2f49fa07652070f87dbe90a7d4ae76 (patch) | |
tree | 546d75173a3066191aab0dd6f32b6fed075c558d | |
parent | cd73720637fb307b11bb67da8d0dc1f74d8edc36 (diff) | |
parent | f81817b47d9682eb89db1dc509a65df656885b44 (diff) | |
download | nextcloud-server-773ce9e58f2f49fa07652070f87dbe90a7d4ae76.tar.gz nextcloud-server-773ce9e58f2f49fa07652070f87dbe90a7d4ae76.zip |
Merge pull request #16613 from nextcloud/enh/featurepolicy
Add Feature-Policy header
20 files changed, 1053 insertions, 2 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 22909fe1712..53d51e998f2 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -37,6 +37,8 @@ return array( 'OCP\\AppFramework\\Http\\DataResponse' => $baseDir . '/lib/public/AppFramework/Http/DataResponse.php', 'OCP\\AppFramework\\Http\\DownloadResponse' => $baseDir . '/lib/public/AppFramework/Http/DownloadResponse.php', 'OCP\\AppFramework\\Http\\EmptyContentSecurityPolicy' => $baseDir . '/lib/public/AppFramework/Http/EmptyContentSecurityPolicy.php', + 'OCP\\AppFramework\\Http\\EmptyFeaturePolicy' => $baseDir . '/lib/public/AppFramework/Http/EmptyFeaturePolicy.php', + 'OCP\\AppFramework\\Http\\FeaturePolicy' => $baseDir . '/lib/public/AppFramework/Http/FeaturePolicy.php', 'OCP\\AppFramework\\Http\\FileDisplayResponse' => $baseDir . '/lib/public/AppFramework/Http/FileDisplayResponse.php', 'OCP\\AppFramework\\Http\\ICallbackResponse' => $baseDir . '/lib/public/AppFramework/Http/ICallbackResponse.php', 'OCP\\AppFramework\\Http\\IOutput' => $baseDir . '/lib/public/AppFramework/Http/IOutput.php', @@ -380,6 +382,7 @@ return array( 'OCP\\Search\\Provider' => $baseDir . '/lib/public/Search/Provider.php', 'OCP\\Search\\Result' => $baseDir . '/lib/public/Search/Result.php', 'OCP\\Security\\CSP\\AddContentSecurityPolicyEvent' => $baseDir . '/lib/public/Security/CSP/AddContentSecurityPolicyEvent.php', + 'OCP\\Security\\FeaturePolicy\\AddFeaturePolicyEvent' => $baseDir . '/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php', 'OCP\\Security\\IContentSecurityPolicyManager' => $baseDir . '/lib/public/Security/IContentSecurityPolicyManager.php', 'OCP\\Security\\ICredentialsManager' => $baseDir . '/lib/public/Security/ICredentialsManager.php', 'OCP\\Security\\ICrypto' => $baseDir . '/lib/public/Security/ICrypto.php', @@ -470,6 +473,7 @@ return array( 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\ReloadExecutionException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/ReloadExecutionException.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\SecurityException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\StrictCookieMissingException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/StrictCookieMissingException.php', + 'OC\\AppFramework\\Middleware\\Security\\FeaturePolicyMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/FeaturePolicyMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\PasswordConfirmationMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\RateLimitingMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\ReloadExecutionMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/ReloadExecutionMiddleware.php', @@ -1107,6 +1111,8 @@ return array( 'OC\\Security\\CertificateManager' => $baseDir . '/lib/private/Security/CertificateManager.php', 'OC\\Security\\CredentialsManager' => $baseDir . '/lib/private/Security/CredentialsManager.php', 'OC\\Security\\Crypto' => $baseDir . '/lib/private/Security/Crypto.php', + 'OC\\Security\\FeaturePolicy\\FeaturePolicy' => $baseDir . '/lib/private/Security/FeaturePolicy/FeaturePolicy.php', + 'OC\\Security\\FeaturePolicy\\FeaturePolicyManager' => $baseDir . '/lib/private/Security/FeaturePolicy/FeaturePolicyManager.php', 'OC\\Security\\Hasher' => $baseDir . '/lib/private/Security/Hasher.php', 'OC\\Security\\IdentityProof\\Key' => $baseDir . '/lib/private/Security/IdentityProof/Key.php', 'OC\\Security\\IdentityProof\\Manager' => $baseDir . '/lib/private/Security/IdentityProof/Manager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 4ddbc050e50..058cede07d9 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -71,6 +71,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\AppFramework\\Http\\DataResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/DataResponse.php', 'OCP\\AppFramework\\Http\\DownloadResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/DownloadResponse.php', 'OCP\\AppFramework\\Http\\EmptyContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/EmptyContentSecurityPolicy.php', + 'OCP\\AppFramework\\Http\\EmptyFeaturePolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/EmptyFeaturePolicy.php', + 'OCP\\AppFramework\\Http\\FeaturePolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/FeaturePolicy.php', 'OCP\\AppFramework\\Http\\FileDisplayResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/FileDisplayResponse.php', 'OCP\\AppFramework\\Http\\ICallbackResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/ICallbackResponse.php', 'OCP\\AppFramework\\Http\\IOutput' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/IOutput.php', @@ -414,6 +416,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Search\\Provider' => __DIR__ . '/../../..' . '/lib/public/Search/Provider.php', 'OCP\\Search\\Result' => __DIR__ . '/../../..' . '/lib/public/Search/Result.php', 'OCP\\Security\\CSP\\AddContentSecurityPolicyEvent' => __DIR__ . '/../../..' . '/lib/public/Security/CSP/AddContentSecurityPolicyEvent.php', + 'OCP\\Security\\FeaturePolicy\\AddFeaturePolicyEvent' => __DIR__ . '/../../..' . '/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php', 'OCP\\Security\\IContentSecurityPolicyManager' => __DIR__ . '/../../..' . '/lib/public/Security/IContentSecurityPolicyManager.php', 'OCP\\Security\\ICredentialsManager' => __DIR__ . '/../../..' . '/lib/public/Security/ICredentialsManager.php', 'OCP\\Security\\ICrypto' => __DIR__ . '/../../..' . '/lib/public/Security/ICrypto.php', @@ -504,6 +507,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\ReloadExecutionException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/ReloadExecutionException.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\SecurityException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\StrictCookieMissingException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/StrictCookieMissingException.php', + 'OC\\AppFramework\\Middleware\\Security\\FeaturePolicyMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/FeaturePolicyMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\PasswordConfirmationMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\RateLimitingMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\ReloadExecutionMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/ReloadExecutionMiddleware.php', @@ -1141,6 +1145,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Security\\CertificateManager' => __DIR__ . '/../../..' . '/lib/private/Security/CertificateManager.php', 'OC\\Security\\CredentialsManager' => __DIR__ . '/../../..' . '/lib/private/Security/CredentialsManager.php', 'OC\\Security\\Crypto' => __DIR__ . '/../../..' . '/lib/private/Security/Crypto.php', + 'OC\\Security\\FeaturePolicy\\FeaturePolicy' => __DIR__ . '/../../..' . '/lib/private/Security/FeaturePolicy/FeaturePolicy.php', + 'OC\\Security\\FeaturePolicy\\FeaturePolicyManager' => __DIR__ . '/../../..' . '/lib/private/Security/FeaturePolicy/FeaturePolicyManager.php', 'OC\\Security\\Hasher' => __DIR__ . '/../../..' . '/lib/private/Security/Hasher.php', 'OC\\Security\\IdentityProof\\Key' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Key.php', 'OC\\Security\\IdentityProof\\Manager' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Manager.php', diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index f47af340b38..89ebc60b226 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -232,6 +232,9 @@ class DIContainer extends SimpleContainer implements IAppContainer { ) ); $dispatcher->registerMiddleware( + $server->query(OC\AppFramework\Middleware\Security\FeaturePolicyMiddleware::class) + ); + $dispatcher->registerMiddleware( new OC\AppFramework\Middleware\Security\PasswordConfirmationMiddleware( $c->query(IControllerMethodReflector::class), $c->query(ISession::class), diff --git a/lib/private/AppFramework/Middleware/Security/FeaturePolicyMiddleware.php b/lib/private/AppFramework/Middleware/Security/FeaturePolicyMiddleware.php new file mode 100644 index 00000000000..9a125edcd9c --- /dev/null +++ b/lib/private/AppFramework/Middleware/Security/FeaturePolicyMiddleware.php @@ -0,0 +1,70 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @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\AppFramework\Middleware\Security; + +use OC\Security\CSP\ContentSecurityPolicyManager; +use OC\Security\CSP\ContentSecurityPolicyNonceManager; +use OC\Security\CSRF\CsrfTokenManager; +use OC\Security\FeaturePolicy\FeaturePolicy; +use OC\Security\FeaturePolicy\FeaturePolicyManager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\EmptyContentSecurityPolicy; +use OCP\AppFramework\Http\EmptyFeaturePolicy; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; + +class FeaturePolicyMiddleware extends Middleware { + + /** @var FeaturePolicyManager */ + private $policyManager; + + public function __construct(FeaturePolicyManager $policyManager) { + $this->policyManager = $policyManager; + } + + /** + * Performs the default FeaturePolicy 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): Response { + $policy = !is_null($response->getFeaturePolicy()) ? $response->getFeaturePolicy() : new FeaturePolicy(); + + if (get_class($policy) === EmptyFeaturePolicy::class) { + return $response; + } + + $defaultPolicy = $this->policyManager->getDefaultPolicy(); + $defaultPolicy = $this->policyManager->mergePolicies($defaultPolicy, $policy); + $response->setFeaturePolicy($defaultPolicy); + + return $response; + } +} diff --git a/lib/private/AppFramework/OCS/BaseResponse.php b/lib/private/AppFramework/OCS/BaseResponse.php index b27784cfcf2..90ea084dd99 100644 --- a/lib/private/AppFramework/OCS/BaseResponse.php +++ b/lib/private/AppFramework/OCS/BaseResponse.php @@ -57,6 +57,8 @@ abstract class BaseResponse extends Response { $statusMessage = null, $itemsCount = null, $itemsPerPage = null) { + parent::__construct(); + $this->format = $format; $this->statusMessage = $statusMessage; $this->itemsCount = $itemsCount; @@ -69,7 +71,6 @@ abstract class BaseResponse extends Response { $this->setETag($dataResponse->getETag()); $this->setLastModified($dataResponse->getLastModified()); $this->setCookies($dataResponse->getCookies()); - $this->setContentSecurityPolicy(new EmptyContentSecurityPolicy()); if ($format === 'json') { $this->addHeader( diff --git a/lib/private/Security/FeaturePolicy/FeaturePolicy.php b/lib/private/Security/FeaturePolicy/FeaturePolicy.php new file mode 100644 index 00000000000..bcfb02bf7c2 --- /dev/null +++ b/lib/private/Security/FeaturePolicy/FeaturePolicy.php @@ -0,0 +1,76 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @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\FeaturePolicy; + +class FeaturePolicy extends \OCP\AppFramework\Http\FeaturePolicy { + + public function getAutoplayDomains(): array { + return $this->autoplayDomains; + } + + public function setAutoplayDomains(array $autoplayDomains): void { + $this->autoplayDomains = $autoplayDomains; + } + + public function getCameraDomains(): array { + return $this->cameraDomains; + } + + public function setCameraDomains(array $cameraDomains): void { + $this->cameraDomains = $cameraDomains; + } + + public function getFullscreenDomains(): array { + return $this->fullscreenDomains; + } + + public function setFullscreenDomains(array $fullscreenDomains): void { + $this->fullscreenDomains = $fullscreenDomains; + } + + public function getGeolocationDomains(): array { + return $this->geolocationDomains; + } + + public function setGeolocationDomains(array $geolocationDomains): void { + $this->geolocationDomains = $geolocationDomains; + } + + public function getMicrophoneDomains(): array { + return $this->microphoneDomains; + } + + public function setMicrophoneDomains(array $microphoneDomains): void { + $this->microphoneDomains = $microphoneDomains; + } + + public function getPaymentDomains(): array { + return $this->paymentDomains; + } + + public function setPaymentDomains(array $paymentDomains): void { + $this->paymentDomains = $paymentDomains; + } +} diff --git a/lib/private/Security/FeaturePolicy/FeaturePolicyManager.php b/lib/private/Security/FeaturePolicy/FeaturePolicyManager.php new file mode 100644 index 00000000000..d5b04a6c16b --- /dev/null +++ b/lib/private/Security/FeaturePolicy/FeaturePolicyManager.php @@ -0,0 +1,76 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @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\FeaturePolicy; + +use OCP\AppFramework\Http\EmptyFeaturePolicy; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Security\FeaturePolicy\AddFeaturePolicyEvent; + +class FeaturePolicyManager { + /** @var EmptyFeaturePolicy[] */ + private $policies = []; + + /** @var IEventDispatcher */ + private $dispatcher; + + public function __construct(IEventDispatcher $dispatcher) { + $this->dispatcher = $dispatcher; + } + + public function addDefaultPolicy(EmptyFeaturePolicy $policy): void { + $this->policies[] = $policy; + } + + public function getDefaultPolicy(): FeaturePolicy { + $event = new AddFeaturePolicyEvent($this); + $this->dispatcher->dispatch(AddFeaturePolicyEvent::class, $event); + + $defaultPolicy = new FeaturePolicy(); + foreach ($this->policies as $policy) { + $defaultPolicy = $this->mergePolicies($defaultPolicy, $policy); + } + return $defaultPolicy; + } + + /** + * Merges the first given policy with the second one + * + */ + public function mergePolicies(FeaturePolicy $defaultPolicy, + EmptyFeaturePolicy $originalPolicy): FeaturePolicy { + 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/public/AppFramework/Http/EmptyFeaturePolicy.php b/lib/public/AppFramework/Http/EmptyFeaturePolicy.php new file mode 100644 index 00000000000..4b8a4f30531 --- /dev/null +++ b/lib/public/AppFramework/Http/EmptyFeaturePolicy.php @@ -0,0 +1,183 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @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 OCP\AppFramework\Http; + +/** + * Class EmptyFeaturePolicy is a simple helper which allows applications + * to modify the FeaturePolicy sent by Nextcloud. Per default the policy + * is forbidding everything. + * + * As alternative with sane exemptions look at FeaturePolicy + * + * @see \OCP\AppFramework\Http\FeaturePolicy + * @package OCP\AppFramework\Http + * @since 17.0.0 + */ +class EmptyFeaturePolicy { + + /** @var string[] of allowed domains to autoplay media */ + protected $autoplayDomains = null; + + /** @var string[] of allowed domains that can access the camera */ + protected $cameraDomains = null; + + /** @var string[] of allowed domains that can use fullscreen */ + protected $fullscreenDomains = null; + + /** @var string[] of allowed domains that can use the geolocation of the device */ + protected $geolocationDomains = null; + + /** @var string[] of allowed domains that can use the microphone */ + protected $microphoneDomains = null; + + /** @var string[] of allowed domains that can use the payment API */ + protected $paymentDomains = null; + + /** + * Allows to use autoplay from a specific domain. Use * to allow from all domains. + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 17.0.0 + */ + public function addAllowedAutoplayDomain(string $domain): self { + $this->autoplayDomains[] = $domain; + return $this; + } + + /** + * Allows to use the camera on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 17.0.0 + */ + public function addAllowedCameraDomain(string $domain): self { + $this->cameraDomains[] = $domain; + return $this; + } + + /** + * Allows the full screen functionality to be used on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 17.0.0 + */ + public function addAllowedFullScreenDomain(string $domain): self { + $this->fullscreenDomains[] = $domain; + return $this; + } + + /** + * Allows to use the geolocation on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 17.0.0 + */ + public function addAllowedGeoLocationDomain(string $domain): self { + $this->geolocationDomains[] = $domain; + return $this; + } + + /** + * Allows to use the microphone on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 17.0.0 + */ + public function addAllowedMicrophoneDomain(string $domain): self { + $this->microphoneDomains[] = $domain; + return $this; + } + + /** + * Allows to use the payment API on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 17.0.0 + */ + public function addAllowedPaymentDomain(string $domain): self { + $this->paymentDomains[] = $domain; + return $this; + } + + /** + * Get the generated Feature-Policy as a string + * + * @return string + * @since 17.0.0 + */ + public function buildPolicy(): string { + $policy = ''; + + if (empty($this->autoplayDomains)) { + $policy .= "autoplay 'none';"; + } else { + $policy .= 'autoplay ' . implode(' ', $this->autoplayDomains); + $policy .= ';'; + } + + if (empty($this->cameraDomains)) { + $policy .= "camera 'none';"; + } else { + $policy .= 'camera ' . implode(' ', $this->cameraDomains); + $policy .= ';'; + } + + if (empty($this->fullscreenDomains)) { + $policy .= "fullscreen 'none';"; + } else { + $policy .= 'fullscreen ' . implode(' ', $this->fullscreenDomains); + $policy .= ';'; + } + + if (empty($this->geolocationDomains)) { + $policy .= "geolocation 'none';"; + } else { + $policy .= 'geolocation ' . implode(' ', $this->geolocationDomains); + $policy .= ';'; + } + + if (empty($this->microphoneDomains)) { + $policy .= "microphone 'none';"; + } else { + $policy .= 'microphone ' . implode(' ', $this->microphoneDomains); + $policy .= ';'; + } + + if (empty($this->paymentDomains)) { + $policy .= "payment 'none';"; + } else { + $policy .= 'payment ' . implode(' ', $this->paymentDomains); + $policy .= ';'; + } + + return rtrim($policy, ';'); + } +} diff --git a/lib/public/AppFramework/Http/FeaturePolicy.php b/lib/public/AppFramework/Http/FeaturePolicy.php new file mode 100644 index 00000000000..98cfae8b2f9 --- /dev/null +++ b/lib/public/AppFramework/Http/FeaturePolicy.php @@ -0,0 +1,59 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @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 OCP\AppFramework\Http; + +/** + * Class FeaturePolicy is a simple helper which allows applications to + * modify the Feature-Policy sent by Nextcloud. Per default only autoplay is allowed + * from the same domain and full screen as well from the same domain. + * + * Even if a value gets modified above defaults will still get appended. Please + * notice that Nextcloud ships already with sensible defaults and those policies + * should require no modification at all for most use-cases. + * + * @package OCP\AppFramework\Http + * @since 17.0.0 + */ +class FeaturePolicy extends EmptyFeaturePolicy { + protected $autoplayDomains = [ + '\'self\'', + ]; + + /** @var string[] of allowed domains that can access the camera */ + protected $cameraDomains = []; + + protected $fullscreenDomains = [ + '\'self\'', + ]; + + /** @var string[] of allowed domains that can use the geolocation of the device */ + protected $geolocationDomains = []; + + /** @var string[] of allowed domains that can use the microphone */ + protected $microphoneDomains = []; + + /** @var string[] of allowed domains that can use the payment API */ + protected $paymentDomains = []; +} diff --git a/lib/public/AppFramework/Http/Response.php b/lib/public/AppFramework/Http/Response.php index 98c0a7f5f70..bfee7d51549 100644 --- a/lib/public/AppFramework/Http/Response.php +++ b/lib/public/AppFramework/Http/Response.php @@ -84,6 +84,9 @@ class Response { /** @var ContentSecurityPolicy|null Used Content-Security-Policy */ private $contentSecurityPolicy = null; + /** @var FeaturePolicy */ + private $featurePolicy; + /** @var bool */ private $throttled = false; /** @var array */ @@ -96,6 +99,7 @@ class Response { */ public function __construct() { $this->setContentSecurityPolicy(new EmptyContentSecurityPolicy()); + $this->setFeaturePolicy(new EmptyFeaturePolicy()); } /** @@ -242,6 +246,7 @@ class Response { $this->setContentSecurityPolicy(new ContentSecurityPolicy()); } $this->headers['Content-Security-Policy'] = $this->contentSecurityPolicy->buildPolicy(); + $this->headers['Feature-Policy'] = $this->featurePolicy->buildPolicy(); if($this->ETag) { $mergeWith['ETag'] = '"' . $this->ETag . '"'; @@ -296,6 +301,24 @@ class Response { /** + * @since 17.0.0 + */ + public function getFeaturePolicy(): EmptyFeaturePolicy { + return $this->featurePolicy; + } + + /** + * @since 17.0.0 + */ + public function setFeaturePolicy(EmptyFeaturePolicy $featurePolicy): self { + $this->featurePolicy = $featurePolicy; + + return $this; + } + + + + /** * Get response status * @since 6.0.0 */ diff --git a/lib/public/AppFramework/Http/TemplateResponse.php b/lib/public/AppFramework/Http/TemplateResponse.php index 334928cc03c..da6f93584b6 100644 --- a/lib/public/AppFramework/Http/TemplateResponse.php +++ b/lib/public/AppFramework/Http/TemplateResponse.php @@ -83,6 +83,7 @@ class TemplateResponse extends Response { $this->renderAs = $renderAs; $this->setContentSecurityPolicy(new ContentSecurityPolicy()); + $this->setFeaturePolicy(new FeaturePolicy()); } diff --git a/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php b/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php new file mode 100644 index 00000000000..ab93844c3f8 --- /dev/null +++ b/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php @@ -0,0 +1,52 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @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 OCP\Security\FeaturePolicy; + +use OC\Security\FeaturePolicy\FeaturePolicyManager; +use OCP\AppFramework\Http\EmptyFeaturePolicy; +use OCP\EventDispatcher\Event; + +/** + * @since 17.0.0 + */ +class AddFeaturePolicyEvent extends Event { + + /** @var FeaturePolicyManager */ + private $policyManager; + + /** + * @since 17.0.0 + */ + public function __construct(FeaturePolicyManager $policyManager) { + $this->policyManager = $policyManager; + } + + /** + * @since 17.0.0 + */ + public function addPolicy(EmptyFeaturePolicy $policy) { + $this->policyManager->addDefaultPolicy($policy); + } +} diff --git a/tests/lib/AppFramework/Controller/ControllerTest.php b/tests/lib/AppFramework/Controller/ControllerTest.php index c37a2a3456c..09d89aa70fd 100644 --- a/tests/lib/AppFramework/Controller/ControllerTest.php +++ b/tests/lib/AppFramework/Controller/ControllerTest.php @@ -117,6 +117,7 @@ class ControllerTest extends \Test\TestCase { 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Content-Type' => 'application/json; charset=utf-8', 'Content-Security-Policy' => "default-src 'none';base-uri 'none';manifest-src 'self'", + 'Feature-Policy' => "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'", ]; $response = $this->controller->customDataResponse(array('hi')); diff --git a/tests/lib/AppFramework/Http/DataResponseTest.php b/tests/lib/AppFramework/Http/DataResponseTest.php index e0eca83f6e9..9cb99b06b65 100644 --- a/tests/lib/AppFramework/Http/DataResponseTest.php +++ b/tests/lib/AppFramework/Http/DataResponseTest.php @@ -69,6 +69,7 @@ class DataResponseTest extends \Test\TestCase { $expectedHeaders = [ 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Content-Security-Policy' => "default-src 'none';base-uri 'none';manifest-src 'self'", + 'Feature-Policy' => "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'", ]; $expectedHeaders = array_merge($expectedHeaders, $headers); diff --git a/tests/lib/AppFramework/Http/EmptyFeaturePolicyTest.php b/tests/lib/AppFramework/Http/EmptyFeaturePolicyTest.php new file mode 100644 index 00000000000..9150503c632 --- /dev/null +++ b/tests/lib/AppFramework/Http/EmptyFeaturePolicyTest.php @@ -0,0 +1,133 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @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 Test\AppFramework\Http; + +use OCP\AppFramework\Http\EmptyFeaturePolicy; + +class EmptyFeaturePolicyTest extends \Test\TestCase { + + /** @var EmptyFeaturePolicy */ + private $policy; + + public function setUp() { + parent::setUp(); + $this->policy = new EmptyFeaturePolicy(); + } + + public function testGetPolicyDefault() { + $defaultPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; + $this->assertSame($defaultPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyAutoplayDomainValid() { + $expectedPolicy = "autoplay www.nextcloud.com;camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedAutoplayDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyAutoplayDomainValidMultiple() { + $expectedPolicy = "autoplay www.nextcloud.com www.nextcloud.org;camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedAutoplayDomain('www.nextcloud.com'); + $this->policy->addAllowedAutoplayDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyCameraDomainValid() { + $expectedPolicy = "autoplay 'none';camera www.nextcloud.com;fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedCameraDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyCameraDomainValidMultiple() { + $expectedPolicy = "autoplay 'none';camera www.nextcloud.com www.nextcloud.org;fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedCameraDomain('www.nextcloud.com'); + $this->policy->addAllowedCameraDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyFullScreenDomainValid() { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen www.nextcloud.com;geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedFullScreenDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyFullScreenDomainValidMultiple() { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen www.nextcloud.com www.nextcloud.org;geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedFullScreenDomain('www.nextcloud.com'); + $this->policy->addAllowedFullScreenDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyGeoLocationDomainValid() { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation www.nextcloud.com;microphone 'none';payment 'none'"; + + $this->policy->addAllowedGeoLocationDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyGeoLocationDomainValidMultiple() { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation www.nextcloud.com www.nextcloud.org;microphone 'none';payment 'none'"; + + $this->policy->addAllowedGeoLocationDomain('www.nextcloud.com'); + $this->policy->addAllowedGeoLocationDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyMicrophoneDomainValid() { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone www.nextcloud.com;payment 'none'"; + + $this->policy->addAllowedMicrophoneDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyMicrophoneDomainValidMultiple() { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone www.nextcloud.com www.nextcloud.org;payment 'none'"; + + $this->policy->addAllowedMicrophoneDomain('www.nextcloud.com'); + $this->policy->addAllowedMicrophoneDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyPaymentDomainValid() { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment www.nextcloud.com"; + + $this->policy->addAllowedPaymentDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyPaymentDomainValidMultiple() { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment www.nextcloud.com www.nextcloud.org"; + + $this->policy->addAllowedPaymentDomain('www.nextcloud.com'); + $this->policy->addAllowedPaymentDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } +} diff --git a/tests/lib/AppFramework/Http/FeaturePolicyTest.php b/tests/lib/AppFramework/Http/FeaturePolicyTest.php new file mode 100644 index 00000000000..7c2cc3fa6d5 --- /dev/null +++ b/tests/lib/AppFramework/Http/FeaturePolicyTest.php @@ -0,0 +1,133 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @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 Test\AppFramework\Http; + +use OCP\AppFramework\Http\FeaturePolicy; + +class FeaturePolicyTest extends \Test\TestCase { + + /** @var EmptyFeaturePolicy */ + private $policy; + + public function setUp() { + parent::setUp(); + $this->policy = new FeaturePolicy(); + } + + public function testGetPolicyDefault() { + $defaultPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; + $this->assertSame($defaultPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyAutoplayDomainValid() { + $expectedPolicy = "autoplay 'self' www.nextcloud.com;camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedAutoplayDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyAutoplayDomainValidMultiple() { + $expectedPolicy = "autoplay 'self' www.nextcloud.com www.nextcloud.org;camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedAutoplayDomain('www.nextcloud.com'); + $this->policy->addAllowedAutoplayDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyCameraDomainValid() { + $expectedPolicy = "autoplay 'self';camera www.nextcloud.com;fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedCameraDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyCameraDomainValidMultiple() { + $expectedPolicy = "autoplay 'self';camera www.nextcloud.com www.nextcloud.org;fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedCameraDomain('www.nextcloud.com'); + $this->policy->addAllowedCameraDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyFullScreenDomainValid() { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self' www.nextcloud.com;geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedFullScreenDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyFullScreenDomainValidMultiple() { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self' www.nextcloud.com www.nextcloud.org;geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedFullScreenDomain('www.nextcloud.com'); + $this->policy->addAllowedFullScreenDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyGeoLocationDomainValid() { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation www.nextcloud.com;microphone 'none';payment 'none'"; + + $this->policy->addAllowedGeoLocationDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyGeoLocationDomainValidMultiple() { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation www.nextcloud.com www.nextcloud.org;microphone 'none';payment 'none'"; + + $this->policy->addAllowedGeoLocationDomain('www.nextcloud.com'); + $this->policy->addAllowedGeoLocationDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyMicrophoneDomainValid() { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone www.nextcloud.com;payment 'none'"; + + $this->policy->addAllowedMicrophoneDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyMicrophoneDomainValidMultiple() { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone www.nextcloud.com www.nextcloud.org;payment 'none'"; + + $this->policy->addAllowedMicrophoneDomain('www.nextcloud.com'); + $this->policy->addAllowedMicrophoneDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyPaymentDomainValid() { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment www.nextcloud.com"; + + $this->policy->addAllowedPaymentDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyPaymentDomainValidMultiple() { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment www.nextcloud.com www.nextcloud.org"; + + $this->policy->addAllowedPaymentDomain('www.nextcloud.com'); + $this->policy->addAllowedPaymentDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } +} diff --git a/tests/lib/AppFramework/Http/ResponseTest.php b/tests/lib/AppFramework/Http/ResponseTest.php index e840111db19..9d6442ea3ce 100644 --- a/tests/lib/AppFramework/Http/ResponseTest.php +++ b/tests/lib/AppFramework/Http/ResponseTest.php @@ -60,6 +60,7 @@ class ResponseTest extends \Test\TestCase { $this->childResponse->setHeaders($expected); $headers = $this->childResponse->getHeaders(); $expected['Content-Security-Policy'] = "default-src 'none';base-uri 'none';manifest-src 'self'"; + $expected['Feature-Policy'] = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; $this->assertEquals($expected, $headers); } @@ -92,7 +93,7 @@ class ResponseTest extends \Test\TestCase { public function testAddHeaderValueNullDeletesIt(){ $this->childResponse->addHeader('hello', 'world'); $this->childResponse->addHeader('hello', null); - $this->assertEquals(2, count($this->childResponse->getHeaders())); + $this->assertEquals(3, count($this->childResponse->getHeaders())); } diff --git a/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php new file mode 100644 index 00000000000..d2ab8d05919 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php @@ -0,0 +1,89 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @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 Test\AppFramework\Middleware\Security; + +use OC\AppFramework\Middleware\Security\FeaturePolicyMiddleware; +use OC\Security\CSP\ContentSecurityPolicy; +use OC\Security\CSRF\CsrfToken; +use OC\Security\FeaturePolicy\FeaturePolicy; +use OC\Security\FeaturePolicy\FeaturePolicyManager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\EmptyContentSecurityPolicy; +use OCP\AppFramework\Http\EmptyFeaturePolicy; +use OCP\AppFramework\Http\Response; +use PHPUnit\Framework\MockObject\MockObject; + +class FeaturePolicyMiddlewareTest extends \Test\TestCase { + + /** @var FeaturePolicyMiddleware|MockObject */ + private $middleware; + /** @var Controller|MockObject */ + private $controller; + /** @var FeaturePolicyManager|MockObject */ + private $manager; + + protected function setUp() { + parent::setUp(); + + $this->controller = $this->createMock(Controller::class); + $this->manager = $this->createMock(FeaturePolicyManager::class); + $this->middleware = new FeaturePolicyMiddleware( + $this->manager + ); + } + + public function testAfterController() { + $response = $this->createMock(Response::class); + $defaultPolicy = new FeaturePolicy(); + $defaultPolicy->addAllowedCameraDomain('defaultpolicy'); + $currentPolicy = new FeaturePolicy(); + $currentPolicy->addAllowedAutoplayDomain('currentPolicy'); + $mergedPolicy = new FeaturePolicy(); + $mergedPolicy->addAllowedGeoLocationDomain('mergedPolicy'); + $response->method('getFeaturePolicy') + ->willReturn($currentPolicy); + $this->manager->method('getDefaultPolicy') + ->willReturn($defaultPolicy); + $this->manager->method('mergePolicies') + ->with($defaultPolicy, $currentPolicy) + ->willReturn($mergedPolicy); + $response->expects($this->once()) + ->method('setFeaturePolicy') + ->with($mergedPolicy); + + $this->middleware->afterController($this->controller, 'test', $response); + } + + public function testAfterControllerEmptyCSP() { + $response = $this->createMock(Response::class); + $emptyPolicy = new EmptyFeaturePolicy(); + $response->method('getFeaturePolicy') + ->willReturn($emptyPolicy); + $response->expects($this->never()) + ->method('setFeaturePolicy'); + + $this->middleware->afterController($this->controller, 'test', $response); + } +} diff --git a/tests/lib/Security/FeaturePolicy/AddFeaturePolicyEventTest.php b/tests/lib/Security/FeaturePolicy/AddFeaturePolicyEventTest.php new file mode 100644 index 00000000000..75525c306ca --- /dev/null +++ b/tests/lib/Security/FeaturePolicy/AddFeaturePolicyEventTest.php @@ -0,0 +1,44 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @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 Test\Security\CSP; + +use OC\Security\FeaturePolicy\FeaturePolicyManager; +use OCP\AppFramework\Http\FeaturePolicy; +use OCP\Security\FeaturePolicy\AddFeaturePolicyEvent; +use Test\TestCase; + +class AddFeaturePolicyEventTest extends TestCase { + public function testAddEvent() { + $manager = $this->createMock(FeaturePolicyManager::class); + $policy = $this->createMock(FeaturePolicy::class); + $event = new AddFeaturePolicyEvent($manager); + + $manager->expects($this->once()) + ->method('addDefaultPolicy') + ->with($policy); + + $event->addPolicy($policy); + } +} diff --git a/tests/lib/Security/FeaturePolicy/FeaturePolicyManagerTest.php b/tests/lib/Security/FeaturePolicy/FeaturePolicyManagerTest.php new file mode 100644 index 00000000000..b4182068b8a --- /dev/null +++ b/tests/lib/Security/FeaturePolicy/FeaturePolicyManagerTest.php @@ -0,0 +1,93 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @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 Test\Security\CSP; + +use OC\Security\CSP\ContentSecurityPolicyManager; +use OC\Security\FeaturePolicy\FeaturePolicyManager; +use OCP\AppFramework\Http\FeaturePolicy; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Security\CSP\AddContentSecurityPolicyEvent; +use OCP\Security\FeaturePolicy\AddFeaturePolicyEvent; +use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Test\TestCase; + +class FeaturePolicyManagerTest extends TestCase { + /** @var EventDispatcherInterface */ + private $dispatcher; + + /** @var FeaturePolicyManager */ + private $manager; + + public function setUp() { + parent::setUp(); + $this->dispatcher = \OC::$server->query(IEventDispatcher::class); + $this->manager = new FeaturePolicyManager($this->dispatcher); + } + + public function testAddDefaultPolicy() { + $this->manager->addDefaultPolicy(new FeaturePolicy()); + $this->addToAssertionCount(1); + } + + public function testGetDefaultPolicyWithPoliciesViaEvent() { + $this->dispatcher->addListener(AddFeaturePolicyEvent::class, function(AddFeaturePolicyEvent $e) { + $policy = new FeaturePolicy(); + $policy->addAllowedMicrophoneDomain('mydomain.com'); + $policy->addAllowedPaymentDomain('mypaymentdomain.com'); + + $e->addPolicy($policy); + }); + + $this->dispatcher->addListener(AddFeaturePolicyEvent::class, function(AddFeaturePolicyEvent $e) { + $policy = new FeaturePolicy(); + $policy->addAllowedPaymentDomain('mydomainother.com'); + $policy->addAllowedGeoLocationDomain('mylocation.here'); + + $e->addPolicy($policy); + }); + + $this->dispatcher->addListener(AddFeaturePolicyEvent::class, function(AddFeaturePolicyEvent $e) { + $policy = new FeaturePolicy(); + $policy->addAllowedAutoplayDomain('youtube.com'); + + $e->addPolicy($policy); + }); + + $expected = new \OC\Security\FeaturePolicy\FeaturePolicy(); + $expected->addAllowedMicrophoneDomain('mydomain.com'); + $expected->addAllowedPaymentDomain('mypaymentdomain.com'); + $expected->addAllowedPaymentDomain('mydomainother.com'); + $expected->addAllowedGeoLocationDomain('mylocation.here'); + $expected->addAllowedAutoplayDomain('youtube.com'); + + $expectedStringPolicy = "autoplay 'self' youtube.com;camera 'none';fullscreen 'self';geolocation mylocation.here;microphone mydomain.com;payment mypaymentdomain.com mydomainother.com"; + + $this->assertEquals($expected, $this->manager->getDefaultPolicy()); + $this->assertSame($expectedStringPolicy, $this->manager->getDefaultPolicy()->buildPolicy()); + } + +} |