createMock(ICacheFactory::class); $cacheFactory ->method('createLocal') ->willReturn(new NullCache()); $ipAddressClassifier = new IpAddressClassifier(); $negativeDnsCache = new NegativeDnsCache($cacheFactory); $this->dnsPinMiddleware = $this->getMockBuilder(DnsPinMiddleware::class) ->setConstructorArgs([$negativeDnsCache, $ipAddressClassifier]) ->onlyMethods(['dnsGetRecord']) ->getMock(); } public function testPopulateDnsCacheIPv4(): void { $mockHandler = new MockHandler([ static function (RequestInterface $request, array $options) { self::arrayHasKey('curl', $options); self::arrayHasKey(CURLOPT_RESOLVE, $options['curl']); self::assertEquals([ 'www.example.com:80:1.1.1.1', 'www.example.com:443:1.1.1.1' ], $options['curl'][CURLOPT_RESOLVE]); return new Response(200); }, ]); $this->dnsPinMiddleware ->method('dnsGetRecord') ->willReturnCallback(function (string $hostname, int $type) { // example.com SOA if ($hostname === 'example.com') { return match ($type) { DNS_SOA => [ [ 'host' => 'example.com', 'class' => 'IN', 'ttl' => 7079, 'type' => 'SOA', 'minimum-ttl' => 3600, ] ], }; } // example.com A, AAAA, CNAME if ($hostname === 'www.example.com') { return match ($type) { DNS_A => [], DNS_AAAA => [], DNS_CNAME => [ [ 'host' => 'www.example.com', 'class' => 'IN', 'ttl' => 1800, 'type' => 'A', 'target' => 'www.example.net' ] ], }; } // example.net SOA if ($hostname === 'example.net') { return match ($type) { DNS_SOA => [ [ 'host' => 'example.net', 'class' => 'IN', 'ttl' => 7079, 'type' => 'SOA', 'minimum-ttl' => 3600, ] ], }; } // example.net A, AAAA, CNAME if ($hostname === 'www.example.net') { return match ($type) { DNS_A => [ [ 'host' => 'www.example.net', 'class' => 'IN', 'ttl' => 1800, 'type' => 'A', 'ip' => '1.1.1.1' ] ], DNS_AAAA => [], DNS_CNAME => [], }; } return false; }); $stack = new HandlerStack($mockHandler); $stack->push($this->dnsPinMiddleware->addDnsPinning()); $handler = $stack->resolve(); $handler( new Request('GET', 'https://www.example.com'), ['nextcloud' => ['allow_local_address' => false]] ); } public function testPopulateDnsCacheIPv6(): void { $mockHandler = new MockHandler([ static function (RequestInterface $request, array $options) { self::arrayHasKey('curl', $options); self::arrayHasKey(CURLOPT_RESOLVE, $options['curl']); self::assertEquals([ 'www.example.com:80:1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001', 'www.example.com:443:1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001' ], $options['curl'][CURLOPT_RESOLVE]); return new Response(200); }, ]); $this->dnsPinMiddleware ->method('dnsGetRecord') ->willReturnCallback(function (string $hostname, int $type) { // example.com SOA if ($hostname === 'example.com') { return match ($type) { DNS_SOA => [ [ 'host' => 'example.com', 'class' => 'IN', 'ttl' => 7079, 'type' => 'SOA', 'minimum-ttl' => 3600, ] ], }; } // example.com A, AAAA, CNAME if ($hostname === 'www.example.com') { return match ($type) { DNS_A => [], DNS_AAAA => [], DNS_CNAME => [ [ 'host' => 'www.example.com', 'class' => 'IN', 'ttl' => 1800, 'type' => 'A', 'target' => 'www.example.net' ] ], }; } // example.net SOA if ($hostname === 'example.net') { return match ($type) { DNS_SOA => [ [ 'host' => 'example.net', 'class' => 'IN', 'ttl' => 7079, 'type' => 'SOA', 'minimum-ttl' => 3600, ] ], }; } // example.net A, AAAA, CNAME if ($hostname === 'www.example.net') { return match ($type) { DNS_A => [ [ 'host' => 'www.example.net', 'class' => 'IN', 'ttl' => 1800, 'type' => 'A', 'ip' => '1.1.1.1' ], [ 'host' => 'www.example.net', 'class' => 'IN', 'ttl' => 1800, 'type' => 'A', 'ip' => '1.0.0.1' ], ], DNS_AAAA => [ [ 'host' => 'www.example.net', 'class' => 'IN', 'ttl' => 1800, 'type' => 'AAAA', 'ip' => '2606:4700:4700::1111' ], [ 'host' => 'www.example.net', 'class' => 'IN', 'ttl' => 1800, 'type' => 'AAAA', 'ip' => '2606:4700:4700::1001' ], ], DNS_CNAME => [], }; } return false; }); $stack = new HandlerStack($mockHandler); $stack->push($this->dnsPinMiddleware->addDnsPinning()); $handler = $stack->resolve(); $handler( new Request('GET', 'https://www.example.com'), ['nextcloud' => ['allow_local_address' => false]] ); } public function testAllowLocalAddress(): void { $mockHandler = new MockHandler([ static function (RequestInterface $request, array $options) { self::assertArrayNotHasKey('curl', $options); return new Response(200); }, ]); $stack = new HandlerStack($mockHandler); $stack->push($this->dnsPinMiddleware->addDnsPinning()); $handler = $stack->resolve(); $handler( new Request('GET', 'https://www.example.com'), ['nextcloud' => ['allow_local_address' => true]] ); } public function testRejectIPv4(): void { $this->expectException(LocalServerException::class); $this->expectExceptionMessage('violates local access rules'); $mockHandler = new MockHandler([ static function (RequestInterface $request, array $options) { // The handler should not be called }, ]); $this->dnsPinMiddleware ->method('dnsGetRecord') ->willReturnCallback(function (string $hostname, int $type) { return match ($type) { DNS_SOA => [ [ 'host' => 'example.com', 'class' => 'IN', 'ttl' => 7079, 'type' => 'SOA', 'minimum-ttl' => 3600, ] ], DNS_A => [ [ 'host' => 'example.com', 'class' => 'IN', 'ttl' => 1800, 'type' => 'A', 'ip' => '192.168.0.1' ] ], DNS_AAAA => [], DNS_CNAME => [], }; }); $stack = new HandlerStack($mockHandler); $stack->push($this->dnsPinMiddleware->addDnsPinning()); $handler = $stack->resolve(); $handler( new Request('GET', 'https://www.example.com'), ['nextcloud' => ['allow_local_address' => false]] ); } public function testRejectIPv6(): void { $this->expectException(LocalServerException::class); $this->expectExceptionMessage('violates local access rules'); $mockHandler = new MockHandler([ static function (RequestInterface $request, array $options) { // The handler should not be called }, ]); $this->dnsPinMiddleware ->method('dnsGetRecord') ->willReturnCallback(function (string $hostname, int $type) { return match ($type) { DNS_SOA => [ [ 'host' => 'example.com', 'class' => 'IN', 'ttl' => 7079, 'type' => 'SOA', 'minimum-ttl' => 3600, ] ], DNS_A => [], DNS_AAAA => [ [ 'host' => 'ipv6.example.com', 'class' => 'IN', 'ttl' => 1800, 'type' => 'AAAA', 'ipv6' => 'fd12:3456:789a:1::1' ] ], DNS_CNAME => [], }; }); $stack = new HandlerStack($mockHandler); $stack->push($this->dnsPinMiddleware->addDnsPinning()); $handler = $stack->resolve(); $handler( new Request('GET', 'https://ipv6.example.com'), ['nextcloud' => ['allow_local_address' => false]] ); } public function testRejectCanonicalName(): void { $this->expectException(LocalServerException::class); $this->expectExceptionMessage('violates local access rules'); $mockHandler = new MockHandler([ static function (RequestInterface $request, array $options) { // The handler should not be called }, ]); $this->dnsPinMiddleware ->method('dnsGetRecord') ->willReturnCallback(function (string $hostname, int $type) { // example.com SOA if ($hostname === 'example.com') { return match ($type) { DNS_SOA => [ [ 'host' => 'example.com', 'class' => 'IN', 'ttl' => 7079, 'type' => 'SOA', 'minimum-ttl' => 3600, ] ], }; } // example.com A, AAAA, CNAME if ($hostname === 'www.example.com') { return match ($type) { DNS_A => [], DNS_AAAA => [], DNS_CNAME => [ [ 'host' => 'www.example.com', 'class' => 'IN', 'ttl' => 1800, 'type' => 'A', 'target' => 'www.example.net' ] ], }; } // example.net SOA if ($hostname === 'example.net') { return match ($type) { DNS_SOA => [ [ 'host' => 'example.net', 'class' => 'IN', 'ttl' => 7079, 'type' => 'SOA', 'minimum-ttl' => 3600, ] ], }; } // example.net A, AAAA, CNAME if ($hostname === 'www.example.net') { return match ($type) { DNS_A => [ [ 'host' => 'www.example.net', 'class' => 'IN', 'ttl' => 1800, 'type' => 'A', 'ip' => '192.168.0.2' ] ], DNS_AAAA => [], DNS_CNAME => [], }; } return false; }); $stack = new HandlerStack($mockHandler); $stack->push($this->dnsPinMiddleware->addDnsPinning()); $handler = $stack->resolve(); $handler( new Request('GET', 'https://www.example.com'), ['nextcloud' => ['allow_local_address' => false]] ); } public function testRejectFaultyResponse(): void { $this->expectException(LocalServerException::class); $this->expectExceptionMessage('No DNS record found for www.example.com'); $mockHandler = new MockHandler([ static function (RequestInterface $request, array $options) { // The handler should not be called }, ]); $this->dnsPinMiddleware ->method('dnsGetRecord') ->willReturnCallback(function (string $hostname, int $type) { return false; }); $stack = new HandlerStack($mockHandler); $stack->push($this->dnsPinMiddleware->addDnsPinning()); $handler = $stack->resolve(); $handler( new Request('GET', 'https://www.example.com'), ['nextcloud' => ['allow_local_address' => false]] ); } public function testIgnoreSubdomainForSoaQuery(): void { $mockHandler = new MockHandler([ static function (RequestInterface $request, array $options) { // The handler should not be called }, ]); $dnsQueries = []; $this->dnsPinMiddleware ->method('dnsGetRecord') ->willReturnCallback(function (string $hostname, int $type) use (&$dnsQueries) { // log query $dnsQueries[] = $hostname . $type; // example.com SOA if ($hostname === 'example.com') { return match ($type) { DNS_SOA => [ [ 'host' => 'example.com', 'class' => 'IN', 'ttl' => 7079, 'type' => 'SOA', 'minimum-ttl' => 3600, ] ], }; } // example.net A, AAAA, CNAME if ($hostname === 'subsubdomain.subdomain.example.com') { return match ($type) { DNS_A => [ [ 'host' => 'subsubdomain.subdomain.example.com', 'class' => 'IN', 'ttl' => 1800, 'type' => 'A', 'ip' => '1.1.1.1' ] ], DNS_AAAA => [], DNS_CNAME => [], }; } return false; }); $stack = new HandlerStack($mockHandler); $stack->push($this->dnsPinMiddleware->addDnsPinning()); $handler = $stack->resolve(); $handler( new Request('GET', 'https://subsubdomain.subdomain.example.com'), ['nextcloud' => ['allow_local_address' => false]] ); $this->assertCount(3, $dnsQueries); $this->assertContains('example.com' . DNS_SOA, $dnsQueries); $this->assertContains('subsubdomain.subdomain.example.com' . DNS_A, $dnsQueries); $this->assertContains('subsubdomain.subdomain.example.com' . DNS_AAAA, $dnsQueries); // CNAME should not be queried if A or AAAA succeeded already $this->assertNotContains('subsubdomain.subdomain.example.com' . DNS_CNAME, $dnsQueries); } }