diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 52a2df0635..3248b42084 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -30,7 +30,28 @@ use Symfony\Component\HttpFoundation\Session\SessionInterface; */ class Request { - protected static $trustProxy = false; + const HEADER_CLIENT_IP = 'client_ip'; + const HEADER_CLIENT_HOST = 'client_host'; + const HEADER_CLIENT_PROTO = 'client_proto'; + const HEADER_CLIENT_PORT = 'client_port'; + + protected static $trustProxyData = false; + + protected static $trustedProxies = array(); + + /** + * Names for headers that can be trusted when + * using trusted proxies. + * + * The default names are non-standard, but widely used + * by popular reverse proxies (like Apache mod_proxy or Amazon EC2). + */ + protected static $trustedHeaders = array( + self::HEADER_CLIENT_IP => 'X_FORWARDED_FOR', + self::HEADER_CLIENT_HOST => 'X_FORWARDED_HOST', + self::HEADER_CLIENT_PROTO => 'X_FORWARDED_PROTO', + self::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT', + ); /** * @var \Symfony\Component\HttpFoundation\ParameterBag @@ -439,14 +460,50 @@ class Request /** * Trusts $_SERVER entries coming from proxies. * - * You should only call this method if your application - * is hosted behind a reverse proxy that you manage. - * - * @api + * @deprecated Deprecated since version 2.0, to be removed in 2.3. Use setTrustedProxies instead. */ public static function trustProxyData() { - self::$trustProxy = true; + self::$trustProxyData = true; + } + + /** + * Sets a list of trusted proxies. + * + * You should only list the reverse proxies that you manage directly. + * + * @param array $proxies A list of trusted proxies + * + * @api + */ + public static function setTrustedProxies(array $proxies) + { + self::$trustedProxies = $proxies; + self::$trustProxyData = $proxies ? true : false; + } + + /** + * Sets the name for trusted headers. + * + * The following header keys are supported: + * + * * Request::HEADER_CLIENT_IP: defaults to X-Forwarded-For (see getClientIp()) + * * Request::HEADER_CLIENT_HOST: defaults to X-Forwarded-Host (see getClientHost()) + * * Request::HEADER_CLIENT_PORT: defaults to X-Forwarded-Port (see getClientPort()) + * * Request::HEADER_CLIENT_PROTO: defaults to X-Forwarded-Proto (see getScheme() and isSecure()) + * + * Setting an empty value allows to disable the trusted header for the given key. + * + * @param string $key The header key + * @param string $value The header name + */ + public static function setTrustedHeaderName($key, $value) + { + if (!array_key_exists($key, self::$trustedHeaders)) { + throw new \InvalidArgumentException(sprintf('Unable to set the trusted header name for key "%s".', $key)); + } + + self::$trustedHeaders[$key] = $value; } /** @@ -582,31 +639,43 @@ class Request /** * Returns the client IP address. * + * This method can read the client IP address from the "X-Forwarded-For" header + * when trusted proxies were set via "setTrustedProxies()". The "X-Forwarded-For" + * header value is a comma+space separated list of IP addresses, the left-most + * being the original client, and each successive proxy that passed the request + * adding the IP address where it received the request from. + * + * If your reverse proxy uses a different header name than "X-Forwarded-For", + * ("Client-Ip" for instance), configure it via "setTrustedHeaderName()" with + * the "client-ip" key. + * * @return string The client IP address * + * @see http://en.wikipedia.org/wiki/X-Forwarded-For + * + * @deprecated The proxy argument is deprecated since version 2.0 and will be removed in 2.3. Use setTrustedProxies instead. + * * @api */ public function getClientIp() { - if (self::$trustProxy) { - if ($this->server->has('HTTP_CLIENT_IP')) { - return $this->server->get('HTTP_CLIENT_IP'); - } elseif ($this->server->has('HTTP_X_FORWARDED_FOR')) { - $clientIp = explode(',', $this->server->get('HTTP_X_FORWARDED_FOR')); + $ip = $this->server->get('REMOTE_ADDR'); - foreach ($clientIp as $ipAddress) { - $cleanIpAddress = trim($ipAddress); - - if (false !== filter_var($cleanIpAddress, FILTER_VALIDATE_IP)) { - return $cleanIpAddress; - } - } - - return ''; - } + if (!self::$trustProxyData) { + return $ip; } - return $this->server->get('REMOTE_ADDR'); + if (!self::$trustedHeaders[self::HEADER_CLIENT_IP] || !$this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP])) { + return $ip; + } + + $clientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP]))); + $clientIps[] = $ip; + + $trustedProxies = self::$trustProxyData && !self::$trustedProxies ? array($ip) : self::$trustedProxies; + $clientIps = array_diff($clientIps, $trustedProxies); + + return array_pop($clientIps); } /** @@ -705,14 +774,22 @@ class Request /** * Returns the port on which the request is made. * + * This method can read the client port from the "X-Forwarded-Port" header + * when trusted proxies were set via "setTrustedProxies()". + * + * The "X-Forwarded-Port" header must contain the client port. + * + * If your reverse proxy uses a different header name than "X-Forwarded-Port", + * configure it via "setTrustedHeaderName()" with the "client-port" key. + * * @return string * * @api */ public function getPort() { - if (self::$trustProxy && $this->headers->has('X-Forwarded-Port')) { - return $this->headers->get('X-Forwarded-Port'); + if (self::$trustProxyData && self::$trustedHeaders[self::HEADER_CLIENT_PORT] && $port = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PORT])) { + return $port; } return $this->server->get('SERVER_PORT'); @@ -858,31 +935,46 @@ class Request /** * Checks whether the request is secure or not. * + * This method can read the client port from the "X-Forwarded-Proto" header + * when trusted proxies were set via "setTrustedProxies()". + * + * The "X-Forwarded-Proto" header must contain the protocol: "https" or "http". + * + * If your reverse proxy uses a different header name than "X-Forwarded-Proto" + * ("SSL_HTTPS" for instance), configure it via "setTrustedHeaderName()" with + * the "client-proto" key. + * * @return Boolean * * @api */ public function isSecure() { - return ( - (strtolower($this->server->get('HTTPS')) == 'on' || $this->server->get('HTTPS') == 1) - || - (self::$trustProxy && strtolower($this->headers->get('SSL_HTTPS')) == 'on' || $this->headers->get('SSL_HTTPS') == 1) - || - (self::$trustProxy && strtolower($this->headers->get('X_FORWARDED_PROTO')) == 'https') - ); + if (self::$trustProxyData && self::$trustedHeaders[self::HEADER_CLIENT_PROTO] && $proto = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PROTO])) { + return in_array(strtolower($proto), array('https', 'on', '1')); + } + + return 'on' == strtolower($this->server->get('HTTPS')) || 1 == $this->server->get('HTTPS'); } /** * Returns the host name. * + * This method can read the client port from the "X-Forwarded-Host" header + * when trusted proxies were set via "setTrustedProxies()". + * + * The "X-Forwarded-Host" header must contain the client host name. + * + * If your reverse proxy uses a different header name than "X-Forwarded-Host", + * configure it via "setTrustedHeaderName()" with the "client-host" key. + * * @return string * * @api */ public function getHost() { - if (self::$trustProxy && $host = $this->headers->get('X_FORWARDED_HOST')) { + if (self::$trustProxyData && self::$trustedHeaders[self::HEADER_CLIENT_HOST] && $host = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_HOST])) { $elements = explode(',', $host); $host = trim($elements[count($elements) - 1]); diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index dad2f6152e..78c5d8be88 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -573,33 +573,14 @@ class RequestTest extends \PHPUnit_Framework_TestCase $request->initialize(array(), array(), array(), array(), array(), array('HTTP_HOST' => 'www.exemple.com')); $this->assertEquals('www.exemple.com', $request->getHost(), '->getHost() from Host Header'); - // Host header with port number. + // Host header with port number $request->initialize(array(), array(), array(), array(), array(), array('HTTP_HOST' => 'www.exemple.com:8080')); $this->assertEquals('www.exemple.com', $request->getHost(), '->getHost() from Host Header with port number'); - // Server values. + // Server values $request->initialize(array(), array(), array(), array(), array(), array('SERVER_NAME' => 'www.exemple.com')); $this->assertEquals('www.exemple.com', $request->getHost(), '->getHost() from server name'); - $this->startTrustingProxyData(); - // X_FORWARDED_HOST. - $request->initialize(array(), array(), array(), array(), array(), array('HTTP_X_FORWARDED_HOST' => 'www.exemple.com')); - $this->assertEquals('www.exemple.com', $request->getHost(), '->getHost() from X_FORWARDED_HOST'); - - // X_FORWARDED_HOST - $request->initialize(array(), array(), array(), array(), array(), array('HTTP_X_FORWARDED_HOST' => 'www.exemple.com, www.second.com')); - $this->assertEquals('www.second.com', $request->getHost(), '->getHost() value from X_FORWARDED_HOST use last value'); - - // X_FORWARDED_HOST with port number - $request->initialize(array(), array(), array(), array(), array(), array('HTTP_X_FORWARDED_HOST' => 'www.exemple.com, www.second.com:8080')); - $this->assertEquals('www.second.com', $request->getHost(), '->getHost() value from X_FORWARDED_HOST with port number'); - - $request->initialize(array(), array(), array(), array(), array(), array('HTTP_HOST' => 'www.exemple.com', 'HTTP_X_FORWARDED_HOST' => 'www.forward.com')); - $this->assertEquals('www.forward.com', $request->getHost(), '->getHost() value from X_FORWARDED_HOST has priority over Host'); - - $request->initialize(array(), array(), array(), array(), array(), array('SERVER_NAME' => 'www.exemple.com', 'HTTP_X_FORWARDED_HOST' => 'www.forward.com')); - $this->assertEquals('www.forward.com', $request->getHost(), '->getHost() value from X_FORWARDED_HOST has priority over SERVER_NAME '); - $request->initialize(array(), array(), array(), array(), array(), array('SERVER_NAME' => 'www.exemple.com', 'HTTP_HOST' => 'www.host.com')); $this->assertEquals('www.host.com', $request->getHost(), '->getHost() value from Host header has priority over SERVER_NAME '); $this->stopTrustingProxyData(); @@ -646,42 +627,40 @@ class RequestTest extends \PHPUnit_Framework_TestCase /** * @dataProvider testGetClientIpProvider */ - public function testGetClientIp($expected, $proxy, $remoteAddr, $httpClientIp, $httpForwardedFor) + public function testGetClientIp($expected, $proxy, $remoteAddr, $httpForwardedFor, $trustedProxies) { $request = new Request(); - $this->assertEquals('', $request->getClientIp()); $server = array('REMOTE_ADDR' => $remoteAddr); - if (null !== $httpClientIp) { - $server['HTTP_CLIENT_IP'] = $httpClientIp; - } if (null !== $httpForwardedFor) { $server['HTTP_X_FORWARDED_FOR'] = $httpForwardedFor; } + if ($proxy || $trustedProxies) { + Request::setTrustedProxies(null === $trustedProxies ? array($remoteAddr) : $trustedProxies); + } + $request->initialize(array(), array(), array(), array(), array(), $server); if ($proxy) { $this->startTrustingProxyData(); } $this->assertEquals($expected, $request->getClientIp($proxy)); - if ($proxy) { - $this->stopTrustingProxyData(); - } + + Request::setTrustedProxies(array()); } public function testGetClientIpProvider() { return array( - array('88.88.88.88', false, '88.88.88.88', null, null), - array('127.0.0.1', false, '127.0.0.1', '88.88.88.88', null), - array('88.88.88.88', true, '127.0.0.1', '88.88.88.88', null), - array('127.0.0.1', false, '127.0.0.1', null, '88.88.88.88'), - array('88.88.88.88', true, '127.0.0.1', null, '88.88.88.88'), - array('::1', false, '::1', null, null), - array('2620:0:1cfe:face:b00c::3', true, '::1', '2620:0:1cfe:face:b00c::3', null), - array('2620:0:1cfe:face:b00c::3', true, '::1', null, '2620:0:1cfe:face:b00c::3, ::1'), - array('88.88.88.88', true, '123.45.67.89', null, '88.88.88.88, 87.65.43.21, 127.0.0.1'), - array('88.88.88.88', true, '123.45.67.89', null, 'unknown, 88.88.88.88'), + array('88.88.88.88', false, '88.88.88.88', null, null), + array('127.0.0.1', false, '127.0.0.1', null, null), + array('::1', false, '::1', null, null), + array('127.0.0.1', false, '127.0.0.1', '88.88.88.88', null), + array('88.88.88.88', true, '127.0.0.1', '88.88.88.88', null), + array('2620:0:1cfe:face:b00c::3', true, '::1', '2620:0:1cfe:face:b00c::3', null), + array('88.88.88.88', true, '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', null), + array('87.65.43.21', true, '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', array('123.45.67.89', '88.88.88.88')), + array('87.65.43.21', false, '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', array('123.45.67.89', '88.88.88.88')), ); } @@ -790,8 +769,9 @@ class RequestTest extends \PHPUnit_Framework_TestCase $this->startTrustingProxyData(); $request->headers->set('X_FORWARDED_PROTO', 'https'); + Request::setTrustedProxies(array('1.1.1.1')); $this->assertTrue($request->isSecure()); - $this->stopTrustingProxyData(); + Request::setTrustedProxies(array()); $request->overrideGlobals(); @@ -997,18 +977,6 @@ class RequestTest extends \PHPUnit_Framework_TestCase $this->assertEquals('foo', $request->getRequestFormat(null)); } - public function testForwardedSecure() - { - $request = new Request(); - $request->headers->set('X-Forwarded-Proto', 'https'); - $request->headers->set('X-Forwarded-Port', 443); - - $this->startTrustingProxyData(); - $this->assertTrue($request->isSecure()); - $this->assertEquals(443, $request->getPort()); - $this->stopTrustingProxyData(); - } - public function testHasSession() { $request = new Request(); @@ -1062,14 +1030,6 @@ class RequestTest extends \PHPUnit_Framework_TestCase ); } - public function testIsProxyTrusted() - { - $this->startTrustingProxyData(); - $this->assertTrue(Request::isProxyTrusted()); - $this->stopTrustingProxyData(); - $this->assertFalse(Request::isProxyTrusted()); - } - public function testIsMethod() { $request = new Request(); @@ -1188,10 +1148,74 @@ class RequestTest extends \PHPUnit_Framework_TestCase private function stopTrustingProxyData() { $class = new \ReflectionClass('Symfony\\Component\\HttpFoundation\\Request'); - $property = $class->getProperty('trustProxy'); + $property = $class->getProperty('trustProxyData'); $property->setAccessible(true); $property->setValue(false); } + + public function testTrustedProxies() + { + $request = Request::create('http://example.com/'); + $request->server->set('REMOTE_ADDR', '3.3.3.3'); + $request->headers->set('X_FORWARDED_FOR', '1.1.1.1, 2.2.2.2'); + $request->headers->set('X_FORWARDED_HOST', 'foo.example.com, real.example.com:8080'); + $request->headers->set('X_FORWARDED_PROTO', 'https'); + $request->headers->set('X_FORWARDED_PORT', 443); + $request->headers->set('X_MY_FOR', '3.3.3.3, 4.4.4.4'); + $request->headers->set('X_MY_HOST', 'my.example.com'); + $request->headers->set('X_MY_PROTO', 'http'); + $request->headers->set('X_MY_PORT', 81); + + // 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()); + + // trusted proxy via deprecated trustProxyData() + Request::trustProxyData(); + $this->assertEquals('2.2.2.2', $request->getClientIp()); + $this->assertEquals('real.example.com', $request->getHost()); + $this->assertEquals(443, $request->getPort()); + $this->assertTrue($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()); + + // 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('real.example.com', $request->getHost()); + $this->assertEquals(443, $request->getPort()); + $this->assertTrue($request->isSecure()); + + // custom header names + Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, 'X_MY_FOR'); + Request::setTrustedHeaderName(Request::HEADER_CLIENT_HOST, 'X_MY_HOST'); + Request::setTrustedHeaderName(Request::HEADER_CLIENT_PORT, 'X_MY_PORT'); + Request::setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, 'X_MY_PROTO'); + $this->assertEquals('4.4.4.4', $request->getClientIp()); + $this->assertEquals('my.example.com', $request->getHost()); + $this->assertEquals(81, $request->getPort()); + $this->assertFalse($request->isSecure()); + + // disabling via empty header names + Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, null); + Request::setTrustedHeaderName(Request::HEADER_CLIENT_HOST, null); + Request::setTrustedHeaderName(Request::HEADER_CLIENT_PORT, null); + Request::setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, null); + $this->assertEquals('3.3.3.3', $request->getClientIp()); + $this->assertEquals('example.com', $request->getHost()); + $this->assertEquals(80, $request->getPort()); + $this->assertFalse($request->isSecure()); + + // reset + Request::setTrustedProxies(array()); + } } class RequestContentProxy extends Request