From 8237712a8f28f3149228755ecb6eeab352529d5b Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Wed, 12 Oct 2022 11:21:56 +0200 Subject: [PATCH 1/2] use HSTS when doing request with the HttpClient Signed-off-by: Roeland Jago Douma --- .../Version26000Date20221011203714.php | 87 ++++++++++ lib/composer/composer/autoload_classmap.php | 3 + lib/composer/composer/autoload_static.php | 3 + lib/private/Http/Client/ClientService.php | 6 +- lib/private/Http/Client/HSTSMiddleware.php | 106 ++++++++++++ lib/private/Http/Client/HSTSStore.php | 157 ++++++++++++++++++ 6 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 core/Migrations/Version26000Date20221011203714.php create mode 100644 lib/private/Http/Client/HSTSMiddleware.php create mode 100644 lib/private/Http/Client/HSTSStore.php diff --git a/core/Migrations/Version26000Date20221011203714.php b/core/Migrations/Version26000Date20221011203714.php new file mode 100644 index 0000000000000..6a1a12110c996 --- /dev/null +++ b/core/Migrations/Version26000Date20221011203714.php @@ -0,0 +1,87 @@ + + * + * @author Your name + * + * @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 . + * + */ + +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 { + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 9cdd19d72334d..3a94569a0d63f 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1062,6 +1062,7 @@ '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 @@ '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', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index fc636f312b5b1..42ff3e85e558b 100644 --- a/lib/composer/composer/autoload_static.php +++ b/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', diff --git a/lib/private/Http/Client/ClientService.php b/lib/private/Http/Client/ClientService.php index e868d4af7a522..ce8798d6f7cf2 100644 --- a/lib/private/Http/Client/ClientService.php +++ b/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 @@ public function newClient(): IClient { $handler = new CurlHandler(); $stack = HandlerStack::create($handler); $stack->push($this->dnsPinMiddleware->addDnsPinning()); + $stack->push($this->HSTSMiddleware->addHSTS()); $client = new GuzzleClient(['handler' => $stack]); diff --git a/lib/private/Http/Client/HSTSMiddleware.php b/lib/private/Http/Client/HSTSMiddleware.php new file mode 100644 index 0000000000000..cdd4025944092 --- /dev/null +++ b/lib/private/Http/Client/HSTSMiddleware.php @@ -0,0 +1,106 @@ + + * + * @author Roeland Jago Douma + * + * @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 . + * + */ +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; + }); + }; + }; + } +} diff --git a/lib/private/Http/Client/HSTSStore.php b/lib/private/Http/Client/HSTSStore.php new file mode 100644 index 0000000000000..db71358cf55cd --- /dev/null +++ b/lib/private/Http/Client/HSTSStore.php @@ -0,0 +1,157 @@ + + * + * @author Roeland Jago Douma + * + * @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 . + * + */ +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(); + } + } +} From 589e7b11a42435eee1c93dacdccfdd591d96cca6 Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Wed, 12 Oct 2022 11:53:03 +0200 Subject: [PATCH 2/2] fixup! use HSTS when doing request with the HttpClient --- .../Migrations/Version26000Date20221011203714.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/core/Migrations/Version26000Date20221011203714.php b/core/Migrations/Version26000Date20221011203714.php index 6a1a12110c996..20ae2ad82bbee 100644 --- a/core/Migrations/Version26000Date20221011203714.php +++ b/core/Migrations/Version26000Date20221011203714.php @@ -37,14 +37,6 @@ */ 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 @@ -77,11 +69,4 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt return $schema; } - /** - * @param IOutput $output - * @param Closure(): ISchemaWrapper $schemaClosure -g * @param array $options - */ - public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { - } }