diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 81eaf408d49a3..d8bf2ad14baa2 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -206,6 +206,15 @@ class Request protected static $requestFactory; + private $isForwardedValid = true; + + private static $forwardedParams = array( + self::HEADER_CLIENT_IP => 'for', + self::HEADER_CLIENT_HOST => 'host', + self::HEADER_CLIENT_PROTO => 'proto', + self::HEADER_CLIENT_PORT => 'host', + ); + /** * Constructor. * @@ -806,41 +815,13 @@ public function setSession(SessionInterface $session) */ public function getClientIps() { - $clientIps = array(); $ip = $this->server->get('REMOTE_ADDR'); if (!$this->isFromTrustedProxy()) { return array($ip); } - $hasTrustedForwardedHeader = self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED]); - $hasTrustedClientIpHeader = self::$trustedHeaders[self::HEADER_CLIENT_IP] && $this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP]); - - if ($hasTrustedForwardedHeader) { - $forwardedHeader = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]); - preg_match_all('{(for)=("?\[?)([a-z0-9\.:_\-/]*)}', $forwardedHeader, $matches); - $forwardedClientIps = $matches[3]; - - $forwardedClientIps = $this->normalizeAndFilterClientIps($forwardedClientIps, $ip); - $clientIps = $forwardedClientIps; - } - - if ($hasTrustedClientIpHeader) { - $xForwardedForClientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP]))); - - $xForwardedForClientIps = $this->normalizeAndFilterClientIps($xForwardedForClientIps, $ip); - $clientIps = $xForwardedForClientIps; - } - - if ($hasTrustedForwardedHeader && $hasTrustedClientIpHeader && $forwardedClientIps !== $xForwardedForClientIps) { - throw new ConflictingHeadersException('The request has both a trusted Forwarded header and a trusted Client IP header, conflicting with each other with regards to the originating IP addresses of the request. This is the result of a misconfiguration. You should either configure your proxy only to send one of these headers, or configure Symfony to distrust one of them.'); - } - - if (!$hasTrustedForwardedHeader && !$hasTrustedClientIpHeader) { - return $this->normalizeAndFilterClientIps(array(), $ip); - } - - return $clientIps; + return $this->getTrustedValues(self::HEADER_CLIENT_IP, $ip) ?: array($ip); } /** @@ -966,31 +947,25 @@ public function getScheme() */ public function getPort() { - if ($this->isFromTrustedProxy()) { - if (self::$trustedHeaders[self::HEADER_CLIENT_PORT] && $port = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PORT])) { - return (int) $port; - } - - if (self::$trustedHeaders[self::HEADER_CLIENT_PROTO] && 'https' === $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PROTO], 'http')) { - return 443; - } + if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_CLIENT_PORT)) { + $host = $host[0]; + } elseif ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_CLIENT_HOST)) { + $host = $host[0]; + } elseif (!$host = $this->headers->get('HOST')) { + return $this->server->get('SERVER_PORT'); } - if ($host = $this->headers->get('HOST')) { - if ($host[0] === '[') { - $pos = strpos($host, ':', strrpos($host, ']')); - } else { - $pos = strrpos($host, ':'); - } - - if (false !== $pos) { - return (int) substr($host, $pos + 1); - } + if ($host[0] === '[') { + $pos = strpos($host, ':', strrpos($host, ']')); + } else { + $pos = strrpos($host, ':'); + } - return 'https' === $this->getScheme() ? 443 : 80; + if (false !== $pos) { + return (int) substr($host, $pos + 1); } - return $this->server->get('SERVER_PORT'); + return 'https' === $this->getScheme() ? 443 : 80; } /** @@ -1190,8 +1165,8 @@ public function getQueryString() */ public function isSecure() { - if ($this->isFromTrustedProxy() && self::$trustedHeaders[self::HEADER_CLIENT_PROTO] && $proto = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PROTO])) { - return in_array(strtolower(current(explode(',', $proto))), array('https', 'on', 'ssl', '1')); + if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_CLIENT_PROTO)) { + return in_array(strtolower($proto[0]), array('https', 'on', 'ssl', '1'), true); } $https = $this->server->get('HTTPS'); @@ -1216,10 +1191,8 @@ public function isSecure() */ public function getHost() { - if ($this->isFromTrustedProxy() && self::$trustedHeaders[self::HEADER_CLIENT_HOST] && $host = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_HOST])) { - $elements = explode(',', $host, 2); - - $host = $elements[0]; + if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_CLIENT_HOST)) { + $host = $host[0]; } elseif (!$host = $this->headers->get('HOST')) { if (!$host = $this->server->get('SERVER_NAME')) { $host = $this->server->get('SERVER_ADDR', ''); @@ -1948,8 +1921,48 @@ private function isFromTrustedProxy() return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR'), self::$trustedProxies); } + private function getTrustedValues($type, $ip = null) + { + $clientValues = array(); + $forwardedValues = array(); + + if (self::$trustedHeaders[$type] && $this->headers->has(self::$trustedHeaders[$type])) { + foreach (explode(',', $this->headers->get(self::$trustedHeaders[$type])) as $v) { + $clientValues[] = (self::HEADER_CLIENT_PORT === $type ? '0.0.0.0:' : '').trim($v); + } + } + + if (self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) { + $forwardedValues = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]); + $forwardedValues = preg_match_all(sprintf('{(?:%s)=(?:"?\[?)([a-zA-Z0-9\.:_\-/]*+)}', self::$forwardedParams[$type]), $forwardedValues, $matches) ? $matches[1] : array(); + } + + if (null !== $ip) { + $clientValues = $this->normalizeAndFilterClientIps($clientValues, $ip); + $forwardedValues = $this->normalizeAndFilterClientIps($forwardedValues, $ip); + } + + if ($forwardedValues === $clientValues || !$clientValues) { + return $forwardedValues; + } + + if (!$forwardedValues) { + return $clientValues; + } + + if (!$this->isForwardedValid) { + return null !== $ip ? array('0.0.0.0', $ip) : array(); + } + $this->isForwardedValid = false; + + throw new ConflictingHeadersException(sprintf('The request has both a trusted "%s" header and a trusted "%s" header, conflicting with each other. You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.', self::$trustedHeaders[self::HEADER_FORWARDED], self::$trustedHeaders[$type])); + } + private function normalizeAndFilterClientIps(array $clientIps, $ip) { + if (!$clientIps) { + return array(); + } $clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from $firstTrustedIp = null; diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index ee753dc6b0778..5687cc62e103c 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -1631,7 +1631,7 @@ private function getRequestInstanceForClientIpsForwardedTests($remoteAddr, $http return $request; } - public function testTrustedProxies() + public function testTrustedProxiesXForwardedFor() { $request = Request::create('http://example.com/'); $request->server->set('REMOTE_ADDR', '3.3.3.3'); @@ -1714,6 +1714,55 @@ public function testTrustedProxies() Request::setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, 'X_FORWARDED_PROTO'); } + public function testTrustedProxiesForwarded() + { + $request = Request::create('http://example.com/'); + $request->server->set('REMOTE_ADDR', '3.3.3.3'); + $request->headers->set('FORWARDED', 'for=1.1.1.1, host=foo.example.com:8080, proto=https, for=2.2.2.2, host=real.example.com:8080'); + + // no trusted proxies + $this->assertEquals('3.3.3.3', $request->getClientIp()); + $this->assertEquals('example.com', $request->getHost()); + $this->assertEquals(80, $request->getPort()); + $this->assertFalse($request->isSecure()); + + // disabling proxy trusting + Request::setTrustedProxies(array()); + $this->assertEquals('3.3.3.3', $request->getClientIp()); + $this->assertEquals('example.com', $request->getHost()); + $this->assertEquals(80, $request->getPort()); + $this->assertFalse($request->isSecure()); + + // request is forwarded by a non-trusted proxy + Request::setTrustedProxies(array('2.2.2.2')); + $this->assertEquals('3.3.3.3', $request->getClientIp()); + $this->assertEquals('example.com', $request->getHost()); + $this->assertEquals(80, $request->getPort()); + $this->assertFalse($request->isSecure()); + + // trusted proxy via setTrustedProxies() + Request::setTrustedProxies(array('3.3.3.3', '2.2.2.2')); + $this->assertEquals('1.1.1.1', $request->getClientIp()); + $this->assertEquals('foo.example.com', $request->getHost()); + $this->assertEquals(8080, $request->getPort()); + $this->assertTrue($request->isSecure()); + + // trusted proxy via setTrustedProxies() + Request::setTrustedProxies(array('3.3.3.4', '2.2.2.2')); + $this->assertEquals('3.3.3.3', $request->getClientIp()); + $this->assertEquals('example.com', $request->getHost()); + $this->assertEquals(80, $request->getPort()); + $this->assertFalse($request->isSecure()); + + // check various X_FORWARDED_PROTO header values + Request::setTrustedProxies(array('3.3.3.3', '2.2.2.2')); + $request->headers->set('FORWARDED', 'proto=ssl'); + $this->assertTrue($request->isSecure()); + + $request->headers->set('FORWARDED', 'proto=https, proto=http'); + $this->assertTrue($request->isSecure()); + } + /** * @expectedException \InvalidArgumentException */ diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ValidateRequestListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ValidateRequestListenerTest.php index 8311a76e35a03..55dc59e13f716 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ValidateRequestListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ValidateRequestListenerTest.php @@ -32,7 +32,7 @@ public function testListenerThrowsWhenMasterRequestHasInconsistentClientIps() $request = new Request(); $request->setTrustedProxies(array('1.1.1.1')); $request->server->set('REMOTE_ADDR', '1.1.1.1'); - $request->headers->set('FORWARDED', '2.2.2.2'); + $request->headers->set('FORWARDED', 'for=2.2.2.2'); $request->headers->set('X_FORWARDED_FOR', '3.3.3.3'); $dispatcher->addListener(KernelEvents::REQUEST, array(new ValidateRequestListener(), 'onKernelRequest')); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php index 6a5b2331f7134..448dc10cf1185 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php @@ -287,7 +287,7 @@ public function testInconsistentClientIpsOnMasterRequests() $request = new Request(); $request->setTrustedProxies(array('1.1.1.1')); $request->server->set('REMOTE_ADDR', '1.1.1.1'); - $request->headers->set('FORWARDED', '2.2.2.2'); + $request->headers->set('FORWARDED', 'for=2.2.2.2'); $request->headers->set('X_FORWARDED_FOR', '3.3.3.3'); $kernel->handle($request, $kernel::MASTER_REQUEST, false);