$server * @param list $trustedProxies */ private function request(array $server, array $trustedProxies = []): Request { return new Request( method: 'GET', path: '/', query: [], post: [], server: $server + ['REQUEST_METHOD' => 'GET', 'SCRIPT_NAME' => '/index.php'], files: [], trustedProxies: $trustedProxies, ); } public function testForwardedHeaderIsIgnoredWithoutTrustedProxies(): void { $request = $this->request([ 'REMOTE_ADDR' => '203.0.113.10', 'HTTP_X_FORWARDED_FOR' => '1.2.3.4', 'HTTP_X_REAL_IP' => '5.6.7.8', ]); self::assertSame('203.0.113.10', $request->clientIp()); } public function testSpoofedHeaderCannotRotateTheIdentity(): void { // Ohne Proxy-Konfiguration muessen alle Versuche dieselbe // Kennung ergeben - sonst waere die Sperre umgehbar. $seen = []; foreach (['1.1.1.1', '2.2.2.2', '3.3.3.3', 'unsinn', ''] as $spoofed) { $seen[] = $this->request([ 'REMOTE_ADDR' => '203.0.113.10', 'HTTP_X_FORWARDED_FOR' => $spoofed, ])->clientIp(); } self::assertSame(['203.0.113.10'], array_values(array_unique($seen))); } public function testForwardedHeaderIsUsedBehindATrustedProxy(): void { $request = $this->request( [ 'REMOTE_ADDR' => '10.0.0.5', 'HTTP_X_FORWARDED_FOR' => '203.0.113.77', ], ['10.0.0.0/8'], ); self::assertSame('203.0.113.77', $request->clientIp()); } public function testChainOfProxiesReturnsTheOutermostUntrustedAddress(): void { $request = $this->request( [ 'REMOTE_ADDR' => '10.0.0.5', 'HTTP_X_FORWARDED_FOR' => '203.0.113.77, 10.0.0.9, 10.0.0.5', ], ['10.0.0.0/8'], ); self::assertSame('203.0.113.77', $request->clientIp()); } public function testInjectedGarbageBehindProxyFallsBackToRemoteAddr(): void { // Ein Angreifer haengt Unsinn an, um die Auswertung zu stoeren. $request = $this->request( [ 'REMOTE_ADDR' => '10.0.0.5', 'HTTP_X_FORWARDED_FOR' => '203.0.113.77, kein-ip', ], ['10.0.0.0/8'], ); self::assertSame('10.0.0.5', $request->clientIp()); } public function testUntrustedRemoteAddrIgnoresForwardedEvenIfProxiesConfigured(): void { $request = $this->request( [ 'REMOTE_ADDR' => '198.51.100.4', 'HTTP_X_FORWARDED_FOR' => '203.0.113.77', ], ['10.0.0.0/8'], ); self::assertSame('198.51.100.4', $request->clientIp()); } public function testPortIsStrippedFromForwardedAddress(): void { $request = $this->request( [ 'REMOTE_ADDR' => '10.0.0.5', 'HTTP_X_FORWARDED_FOR' => '203.0.113.77:51234', ], ['10.0.0.0/8'], ); self::assertSame('203.0.113.77', $request->clientIp()); } public function testMissingRemoteAddrIsHandled(): void { self::assertSame('0.0.0.0', $this->request([])->clientIp()); self::assertSame('0.0.0.0', $this->request(['REMOTE_ADDR' => 'quatsch'])->clientIp()); } // --- HTTPS-Erkennung --------------------------------------------- public function testForwardedProtoIsOnlyTrustedBehindAProxy(): void { $spoofed = $this->request([ 'REMOTE_ADDR' => '203.0.113.10', 'HTTP_X_FORWARDED_PROTO' => 'https', ]); self::assertFalse($spoofed->isSecure()); $behindProxy = $this->request( [ 'REMOTE_ADDR' => '10.0.0.5', 'HTTP_X_FORWARDED_PROTO' => 'https', ], ['10.0.0.0/8'], ); self::assertTrue($behindProxy->isSecure()); } public function testDirectHttpsIsDetected(): void { self::assertTrue($this->request(['HTTPS' => 'on'])->isSecure()); self::assertTrue($this->request(['SERVER_PORT' => '443'])->isSecure()); self::assertFalse($this->request(['HTTPS' => 'off'])->isSecure()); self::assertFalse($this->request([])->isSecure()); } // --- Host-Header -------------------------------------------------- /** * @return array */ public static function hostProvider(): array { return [ 'einfach' => ['kurz.example.de', 'kurz.example.de'], 'mit Port' => ['kurz.example.de:8080', 'kurz.example.de:8080'], 'Grossbuchstaben' => ['Kurz.Example.DE', 'kurz.example.de'], 'IPv6' => ['[2001:db8::1]', '[2001:db8::1]'], 'Injektion mit Slash' => ['example.de/pfad', ''], 'Injektion mit Zeilenumbruch' => ["example.de\r\nX-Fake: 1", ''], 'Injektion mit Leerzeichen' => ['example.de boese.de', ''], 'Injektion mit @' => ['boese.de@example.de', ''], 'Injektion mit Anfuehrungszeichen' => ['example.de">