Browse Source

use HSTS when doing request with the HttpClient

Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
Roeland Jago Douma 1 year ago
parent
commit
8237712a8f

+ 87 - 0
core/Migrations/Version26000Date20221011203714.php

@@ -0,0 +1,87 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2022 Your name <your@email.com>
+ *
+ * @author Your name <your@email.com>
+ *
+ * @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\Core\Migrations;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+/**
+ * Auto-generated migration step: Please modify to your needs!
+ */
+class Version26000Date20221011203714 extends SimpleMigrationStep {
+
+	/**
+	 * @param IOutput $output
+	 * @param Closure(): ISchemaWrapper $schemaClosure
+	 * @param array $options
+	 */
+	public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+	}
+
+	/**
+	 * @param IOutput $output
+	 * @param Closure(): ISchemaWrapper $schemaClosure
+	 * @param array $options
+	 * @return null|ISchemaWrapper
+	 */
+	public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+		$schema = $schemaClosure();
+
+		if (!$schema->hasTable('hsts')) {
+			$table = $schema->createTable('hsts');
+			$table->addColumn('id', Types::BIGINT, [
+				'autoincrement' => true,
+				'notnull' => true,
+			]);
+			$table->addColumn('host', Types::STRING, [
+				'notnull' => true,
+				'length' => 255,
+			]);
+			$table->addColumn('expires', Types::BIGINT, [
+				'notnull' => true,
+			]);
+			$table->addColumn('includeSubdomains', Types::BOOLEAN, [
+				'notnull' => false,
+			]);
+			$table->setPrimaryKey(['id'], 'hsts_idx');
+			$table->addUniqueConstraint(['host'], 'hsts_host');
+		}
+
+		return $schema;
+	}
+
+	/**
+	 * @param IOutput $output
+	 * @param Closure(): ISchemaWrapper $schemaClosure
+g	 * @param array $options
+	 */
+	public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+	}
+}

+ 3 - 0
lib/composer/composer/autoload_classmap.php

@@ -1062,6 +1062,7 @@ return array(
     'OC\\Core\\Migrations\\Version24000Date20220425072957' => $baseDir . '/core/Migrations/Version24000Date20220425072957.php',
     'OC\\Core\\Migrations\\Version25000Date20220515204012' => $baseDir . '/core/Migrations/Version25000Date20220515204012.php',
     'OC\\Core\\Migrations\\Version25000Date20220602190540' => $baseDir . '/core/Migrations/Version25000Date20220602190540.php',
+    'OC\\Core\\Migrations\\Version26000Date20221011203714' => $baseDir . '/core/Migrations/Version26000Date20221011203714.php',
     'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
     'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
     'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php',
@@ -1280,6 +1281,8 @@ return array(
     'OC\\Http\\Client\\Client' => $baseDir . '/lib/private/Http/Client/Client.php',
     'OC\\Http\\Client\\ClientService' => $baseDir . '/lib/private/Http/Client/ClientService.php',
     'OC\\Http\\Client\\DnsPinMiddleware' => $baseDir . '/lib/private/Http/Client/DnsPinMiddleware.php',
+    'OC\\Http\\Client\\HSTSMiddleware' => $baseDir . '/lib/private/Http/Client/HSTSMiddleware.php',
+    'OC\\Http\\Client\\HSTSStore' => $baseDir . '/lib/private/Http/Client/HSTSStore.php',
     'OC\\Http\\Client\\LocalAddressChecker' => $baseDir . '/lib/private/Http/Client/LocalAddressChecker.php',
     'OC\\Http\\Client\\NegativeDnsCache' => $baseDir . '/lib/private/Http/Client/NegativeDnsCache.php',
     'OC\\Http\\Client\\Response' => $baseDir . '/lib/private/Http/Client/Response.php',

+ 3 - 0
lib/composer/composer/autoload_static.php

@@ -1095,6 +1095,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OC\\Core\\Migrations\\Version24000Date20220425072957' => __DIR__ . '/../../..' . '/core/Migrations/Version24000Date20220425072957.php',
         'OC\\Core\\Migrations\\Version25000Date20220515204012' => __DIR__ . '/../../..' . '/core/Migrations/Version25000Date20220515204012.php',
         'OC\\Core\\Migrations\\Version25000Date20220602190540' => __DIR__ . '/../../..' . '/core/Migrations/Version25000Date20220602190540.php',
+        'OC\\Core\\Migrations\\Version26000Date20221011203714' => __DIR__ . '/../../..' . '/core/Migrations/Version26000Date20221011203714.php',
         'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
         'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
         'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php',
@@ -1313,6 +1314,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OC\\Http\\Client\\Client' => __DIR__ . '/../../..' . '/lib/private/Http/Client/Client.php',
         'OC\\Http\\Client\\ClientService' => __DIR__ . '/../../..' . '/lib/private/Http/Client/ClientService.php',
         'OC\\Http\\Client\\DnsPinMiddleware' => __DIR__ . '/../../..' . '/lib/private/Http/Client/DnsPinMiddleware.php',
+        'OC\\Http\\Client\\HSTSMiddleware' => __DIR__ . '/../../..' . '/lib/private/Http/Client/HSTSMiddleware.php',
+        'OC\\Http\\Client\\HSTSStore' => __DIR__ . '/../../..' . '/lib/private/Http/Client/HSTSStore.php',
         'OC\\Http\\Client\\LocalAddressChecker' => __DIR__ . '/../../..' . '/lib/private/Http/Client/LocalAddressChecker.php',
         'OC\\Http\\Client\\NegativeDnsCache' => __DIR__ . '/../../..' . '/lib/private/Http/Client/NegativeDnsCache.php',
         'OC\\Http\\Client\\Response' => __DIR__ . '/../../..' . '/lib/private/Http/Client/Response.php',

+ 5 - 1
lib/private/Http/Client/ClientService.php

@@ -48,15 +48,18 @@ class ClientService implements IClientService {
 	private $dnsPinMiddleware;
 	/** @var LocalAddressChecker */
 	private $localAddressChecker;
+	private HSTSMiddleware $HSTSMiddleware;
 
 	public function __construct(IConfig $config,
 								ICertificateManager $certificateManager,
 								DnsPinMiddleware $dnsPinMiddleware,
-								LocalAddressChecker $localAddressChecker) {
+								LocalAddressChecker $localAddressChecker,
+								HSTSMiddleware $HSTSMiddleware) {
 		$this->config = $config;
 		$this->certificateManager = $certificateManager;
 		$this->dnsPinMiddleware = $dnsPinMiddleware;
 		$this->localAddressChecker = $localAddressChecker;
+		$this->HSTSMiddleware = $HSTSMiddleware;
 	}
 
 	/**
@@ -66,6 +69,7 @@ class ClientService implements IClientService {
 		$handler = new CurlHandler();
 		$stack = HandlerStack::create($handler);
 		$stack->push($this->dnsPinMiddleware->addDnsPinning());
+		$stack->push($this->HSTSMiddleware->addHSTS());
 
 		$client = new GuzzleClient(['handler' => $stack]);
 

+ 106 - 0
lib/private/Http/Client/HSTSMiddleware.php

@@ -0,0 +1,106 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2022, 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\Http\Client;
+
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Log\LoggerInterface;
+
+class HSTSMiddleware {
+
+    private HSTSStore $hstsStore;
+    private LoggerInterface $logger;
+
+	public function __construct(
+        HSTSStore $hstsStore,
+        LoggerInterface $logger
+	) {
+        $this->hstsStore = $hstsStore;
+        $this->logger = $logger;
+	}
+
+    private function isIpaAddr(string $host): bool {
+        return filter_var($host, FILTER_VALIDATE_IP) !== false;
+    }
+
+    private function handleHSTSRewrite(RequestInterface $request): RequestInterface {
+
+        $uri = $request->getUri();
+
+        if ($uri->getScheme() === 'http'
+            && !$this->isIpaAddr($uri->getHost())
+            && $this->hstsStore->hasHSTS($uri->getHost())) {
+            
+            $uri = $uri->withScheme('https');
+        }
+
+        return $request->withUri($uri);
+    }
+
+    private function handleHSTSResponse(ResponseInterface $response, RequestInterface $request): ResponseInterface {
+        $uri = $request->getUri();
+
+        $this->logger->error($uri->getScheme());
+
+        if ($uri->getScheme() === 'https'
+            && !$this->isIpaAddr($uri->getHost())
+            && $response->hasHeader('Strict-Transport-Security')) {
+            
+
+                $this->logger->error("LETS GO");
+
+            // Get the header and pass it to the store to parse and store this info
+            $header = $response->getHeader('Strict-Transport-Security')[0];
+            $this->hstsStore->setHSTS($uri->getHost(), $header);
+        }
+
+        return $response;
+    }
+
+	public function addHSTS() {
+		return function (callable $handler) {
+			return function (
+				RequestInterface $request,
+				array $options
+			) use ($handler) {
+
+                $request = $this->handleHSTSRewrite($request);
+
+                $this->logger->warning("GONNA REQUEST");
+                $this->logger->warning($request->getUri()->getScheme());
+                $this->logger->warning($request->getUri()->getHost());
+
+
+				return $handler($request, $options)
+                    ->then(function (ResponseInterface $response) use ($request) {
+                        $this->logger->error("GOT RESPONSE");
+                        $this->handleHSTSResponse($response, $request);
+                        return $response;
+                    });
+			};
+		};
+	}
+}

+ 157 - 0
lib/private/Http/Client/HSTSStore.php

@@ -0,0 +1,157 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2022, 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\Http\Client;
+
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IDBConnection;
+use Psr\Log\LoggerInterface;
+
+class HSTSStore {
+
+	private IDBConnection $db;
+	private ITimeFactory $timeFactory;
+	private LoggerInterface $logger;
+
+	public function __construct(IDBConnection $db, ITimeFactory $timeFactory, LoggerInterface $logger) {
+		$this->db = $db;
+		$this->timeFactory = $timeFactory;
+		$this->logger = $logger;
+	}
+
+	private function checkHost(string $host, bool $includeSubdomain) {
+		// Look for the domain as is if we can't find it remove a subdomain and go up
+
+		$this->logger->warning("Checking for host " . $host);
+
+		$qb = $this->db->getQueryBuilder();
+		$qb->select('*')
+		->from('hsts')
+		->where($qb->expr()->eq('host', $qb->createNamedParameter($host)));
+
+		$cursor = $qb->executeQuery();
+		$data = $cursor->fetch();
+		$cursor->closeCursor();
+
+		if ($data !== false) {
+			$this->logger->warning("GOT DATA");
+			$this->logger->warning(json_encode($data));
+		}
+
+		if ($data !== false 
+			&& $this->timeFactory->getTime() < $data['expires']
+			&& (!$includeSubdomain || ($includeSubdomain && $data['includeSubdomains']))
+			) {
+			$this->logger->warning("REWRITE");
+			return true;
+		}
+
+		return false;
+	}
+
+	private function checkSuperHost(string $host): bool {
+		$labels = explode('.', $host);
+
+		$labelCount = count($labels);
+
+		for ($i = 1; $i < $labelCount; $i++) {
+			$domainName = implode('.', array_slice($labels, $labelCount - $i));
+
+            if ($this->checkHost($domainName, true)) {
+                return true;
+            }
+		}
+		
+		return false;
+	}
+
+	public function hasHSTS(string $host): bool {
+		return $this->checkHost($host, false) || $this->checkSuperHost($host);
+	}
+
+	public function setHSTS(string $host, string $header): void {
+		$directives = explode(';', $header);
+
+		$maxAge = 0;
+		$includeSubdomains = false;
+
+		foreach ($directives as $directive) {
+			$directive = trim($directive);
+
+			if ($directive === 'includeSubDomains') {
+				$includeSubdomains = true;
+			} elseif ($directive === 'preload') {
+				// We just ignore this
+			} else {
+				$data = explode('=', $directive);
+				if (count($data) === 2 && trim($data[0]) === 'max-age' && is_numeric(trim($data[1]))) {
+					$maxAge = max(0, (int)$data[1]);
+				}
+			}
+		}
+
+		if ($maxAge <= 0) {
+			return;
+		}
+
+		$this->logger->warning("TIME TO SET HSTS");
+
+		$expires = $this->timeFactory->getTime() + $maxAge;
+
+		$qb = $this->db->getQueryBuilder();
+		$qb->select('*')
+			->from('hsts')
+			->where($qb->expr()->eq('host', $qb->createNamedParameter($host)));
+		
+		$cursor = $qb->executeQuery();
+		$data = $cursor->fetchOne();
+		$cursor->closeCursor();
+
+
+		$this->logger->warning("Q1");
+
+		if ($data === false) {
+			// No entry yet insert
+			$qb = $this->db->getQueryBuilder();
+			$qb->insert('hsts')
+			->values([
+				'host' => $qb->createNamedParameter($host),
+				'expires' => $qb->createNamedParameter($expires),
+				'includeSubdomains' => $qb->createNamedParameter($includeSubdomains)
+			]);
+			$this->logger->warning($qb->getSQL());
+			$qb->executeStatement();
+		} else {
+			// Already set just update
+			// No entry yet insert
+			$qb = $this->db->getQueryBuilder();
+			$qb->update('hsts')
+			->set('expires', $qb->createNamedParameter($expires))
+			->set('includeSubdomains', $qb->createNamedParameter($includeSubdomains))
+			->where($qb->expr()->eq('host', $qb->createNamedParameter($host)));
+			$qb->executeStatement();
+		}
+	}
+}