diff options
author | Julius Härtl <jus@bitgrid.net> | 2021-06-29 22:46:33 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-29 22:46:33 +0300 |
commit | b6fc3ea7ef4e242288f5b400fc5abe1c53ef4bf9 (patch) | |
tree | 760f7fab825596553abf1320d84f17a1d8a8e47d | |
parent | 142ba8bcdfde0afccea43a72d6a137e659d329ba (diff) | |
parent | b62d9fb40de982509526c42a4a3b076f8772fc24 (diff) |
Merge pull request #1641 from nextcloud/backport/1640/stable3.8
[stable3.8] Allow to limit wopi requests to specific source hosts
-rw-r--r-- | lib/AppInfo/Application.php | 2 | ||||
-rw-r--r-- | lib/Controller/SettingsController.php | 6 | ||||
-rw-r--r-- | lib/Middleware/WOPIMiddleware.php | 142 | ||||
-rw-r--r-- | lib/Settings/Admin.php | 1 | ||||
-rw-r--r-- | src/components/AdminSettings.vue | 11 |
5 files changed, 162 insertions, 0 deletions
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index ecd3d7ce..3c105863 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -29,6 +29,7 @@ use OC\Security\CSP\ContentSecurityPolicy; use OCA\Federation\TrustedServers; use OCA\Richdocuments\AppConfig; use OCA\Richdocuments\Capabilities; +use OCA\Richdocuments\Middleware\WOPIMiddleware; use OCA\Richdocuments\Preview\MSExcel; use OCA\Richdocuments\Preview\MSWord; use OCA\Richdocuments\Preview\OOXML; @@ -77,6 +78,7 @@ class Application extends App { } $this->getContainer()->registerCapability(Capabilities::class); + $this->getContainer()->registerMiddleWare(WOPIMiddleware::class); } public function registerProvider() { diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index ce73dd1d..bec87a4b 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -111,6 +111,7 @@ class SettingsController extends Controller{ public function getSettings() { return new JSONResponse([ 'wopi_url' => $this->appConfig->getAppValue('wopi_url'), + 'wopi_allowlist' => $this->appConfig->getAppValue('wopi_allowlist'), 'public_wopi_url' => $this->appConfig->getAppValue('public_wopi_url'), 'disable_certificate_verification' => $this->appConfig->getAppValue('disable_certificate_verification') === 'yes', 'edit_groups' => $this->appConfig->getAppValue('edit_groups'), @@ -130,6 +131,7 @@ class SettingsController extends Controller{ * @return JSONResponse */ public function setSettings($wopi_url, + $wopi_allowlist, $disable_certificate_verification, $edit_groups, $use_groups, @@ -142,6 +144,10 @@ class SettingsController extends Controller{ $this->appConfig->setAppValue('wopi_url', $wopi_url); } + if ($wopi_allowlist !== null){ + $this->appConfig->setAppValue('wopi_allowlist', $wopi_allowlist); + } + if ($disable_certificate_verification !== null) { $this->appConfig->setAppValue( 'disable_certificate_verification', diff --git a/lib/Middleware/WOPIMiddleware.php b/lib/Middleware/WOPIMiddleware.php new file mode 100644 index 00000000..9f6420e9 --- /dev/null +++ b/lib/Middleware/WOPIMiddleware.php @@ -0,0 +1,142 @@ +<?php +/* + * @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net> + * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> + * + * @author Julius Härtl <jus@bitgrid.net> + * @author Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +declare(strict_types=1); + +namespace OCA\Richdocuments\Middleware; + +use OCA\Richdocuments\AppInfo\Application; +use OCA\Richdocuments\Controller\WopiController; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; +use OCP\Files\NotPermittedException; +use OCP\IConfig; +use OCP\IRequest; + +class WOPIMiddleware extends Middleware { + + /** @var IConfig */ + private $config; + /** @var IRequest */ + private $request; + + public function __construct(IConfig $config, IRequest $request) { + $this->config = $config; + $this->request = $request; + } + + public function beforeController($controller, $methodName) { + parent::beforeController($controller, $methodName); + + if ($controller instanceof WopiController && !$this->isWOPIAllowed()) { + throw new NotPermittedException(); + } + } + + public function afterException($controller, $methodName, \Exception $exception): Response { + if ($exception instanceof NotPermittedException && $controller instanceof WopiController) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + + throw $exception; + } + + public function isWOPIAllowed(): bool { + $allowedRanges = $this->config->getAppValue(Application::APPNAME, 'wopi_allowlist'); + if($allowedRanges === '') { + return true; + } + $allowedRanges = explode(',', $allowedRanges); + + $userIp = $this->request->getRemoteAddress(); + foreach($allowedRanges as $range) { + if($this->matchCidr($userIp, $range)) { + return true; + } + } + + return false; + } + + /** + * @copyright https://stackoverflow.com/questions/594112/matching-an-ip-to-a-cidr-mask-in-php-5/594134#594134 + * @copyright (IPv4) https://stackoverflow.com/questions/594112/matching-an-ip-to-a-cidr-mask-in-php-5/594134#594134 + * @copyright (IPv6) MW. https://stackoverflow.com/questions/7951061/matching-ipv6-address-to-a-cidr-subnet via + */ + private function matchCidr(string $ip, string $range): bool { + list($subnet, $bits) = array_pad(explode('/', $range), 2, null); + if ($bits === null) { + $bits = 32; + } + $bits = (int)$bits; + + if ($this->isIpv4($ip) && $this->isIpv4($subnet)) { + $mask = -1 << (32 - $bits); + + $ip = ip2long($ip); + $subnet = ip2long($subnet); + $subnet &= $mask; + return ($ip & $mask) === $subnet; + } + + if ($this->isIpv6($ip) && $this->isIPv6($subnet)) { + $subnet = inet_pton($subnet); + $ip = inet_pton($ip); + + $binMask = str_repeat("f", $bits / 4); + switch ($bits % 4) { + case 0: + break; + case 1: + $binMask .= "8"; + break; + case 2: + $binMask .= "c"; + break; + case 3: + $binMask .= "e"; + break; + } + + $binMask = str_pad($binMask, 32, '0'); + $binMask = pack("H*", $binMask); + + if ( ($ip & $binMask) === $subnet ) { + return true; + } + } + return false; + } + + private function isIpv4($ip) { + return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); + } + + private function isIpv6($ip) { + return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); + } +} diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 8dc8c3bd..01fea6bd 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -80,6 +80,7 @@ class Admin implements ISettings { [ 'settings' => [ 'wopi_url' => $this->config->getAppValue('richdocuments', 'wopi_url'), + 'wopi_allowlist' => $this->config->getAppValue('richdocuments', 'wopi_allowlist'), 'edit_groups' => $this->config->getAppValue('richdocuments', 'edit_groups'), 'use_groups' => $this->config->getAppValue('richdocuments', 'use_groups'), 'doc_format' => $this->config->getAppValue('richdocuments', 'doc_format'), diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index c0b0f345..1ceaeabe 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -192,6 +192,12 @@ @input="updateExternalApps" /> </div> </settings-checkbox> + + <settings-input-text v-model="settings.wopi_allowlist" + label="Allow list for WOPI requests" + :hint="t('richdocuments', 'List of IPV4 and IPV6 IP-addresses and subnets that are allowed to perform requests ot the WOPI endpoints. If no allow list is specified all hosts will be allowed. E.g. 10.0.0.20,10.0.4.0/24')" + :disabled="updating" + @update="updateWopiAllowlist" /> </div> <div v-if="isSetup" id="secure-view-settings" class="section"> @@ -426,6 +432,11 @@ export default { external_apps: this.settings.external_apps }) }, + async updateWopiAllowlist(allowlist) { + await this.updateSettings({ + wopi_allowlist: allowlist + }) + }, async updateOoxml(enabled) { this.settings.doc_format = enabled ? 'ooxml' : '' await this.updateSettings({ |