From 67e12f3ecbc6622b20dcc73014a597bacfd07e34 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 28 Nov 2012 08:25:47 +0100 Subject: [PATCH] added a way to configure the X-Forwarded-XXX header names and a way to disable trusting them --- .../Component/HttpFoundation/Request.php | 79 +++++++++++++++++-- .../Component/HttpFoundation/RequestTest.php | 41 +++++++++- 2 files changed, 111 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 34249c624a6e..adbff82f242f 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -24,6 +24,20 @@ class Request 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( + 'client_ip' => 'X_FORWARDED_FOR', + 'client_host' => 'X_FORWARDED_HOST', + 'client_proto' => 'X_FORWARDED_PROTO', + 'client_port' => 'X_FORWARDED_PORT', + ); + /** * @var \Symfony\Component\HttpFoundation\ParameterBag * @@ -381,6 +395,30 @@ public static function setTrustedProxies(array $proxies) self::$trustProxyData = $proxies ? true : false; } + /** + * Sets the name for trusted headers. + * + * The following header keys are supported: + * + * * client_ip: defaults to X-Forwarded-For (see getClientIp()) + * * client_host: defaults to X-Forwarded-Host (see getClientHost()) + * * client_port: defaults to X-Forwarded-Port (see getClientPort()) + * * 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; + } + /** * Gets a "parameter" value. * @@ -461,6 +499,10 @@ public function setSession(Session $session) * 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. + * * @param Boolean $proxy Whether the current request has been made behind a proxy or not (deprecated) * * @return string The client IP address @@ -479,11 +521,11 @@ public function getClientIp($proxy = false) return $ip; } - if (!$this->server->has('HTTP_X_FORWARDED_FOR')) { + if (!self::$trustedHeaders['client_ip'] || !$this->headers->has(self::$trustedHeaders['client_ip'])) { return $ip; } - $clientIps = array_map('trim', explode(',', $this->server->get('HTTP_X_FORWARDED_FOR'))); + $clientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders['client_ip']))); $clientIps[] = $ip; $trustedProxies = ($proxy || self::$trustProxyData) && !self::$trustedProxies ? array($ip) : self::$trustedProxies; @@ -586,14 +628,22 @@ public function getScheme() /** * 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::$trustProxyData && $this->headers->has('X-Forwarded-Port')) { - return $this->headers->get('X-Forwarded-Port'); + if (self::$trustProxyData && self::$trustedHeaders['client_port'] && $port = $this->headers->get(self::$trustedHeaders['client_port'])) { + return $port; } return $this->server->get('SERVER_PORT'); @@ -706,6 +756,15 @@ public function getQueryString() /** * 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 @@ -717,20 +776,28 @@ public function isSecure() || (self::$trustProxyData && strtolower($this->headers->get('SSL_HTTPS')) == 'on' || $this->headers->get('SSL_HTTPS') == 1) || - (self::$trustProxyData && strtolower($this->headers->get('X_FORWARDED_PROTO')) == 'https') + (self::$trustProxyData && self::$trustedHeaders['client_proto'] && strtolower($this->headers->get(self::$trustedHeaders['client_proto'])) == '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::$trustProxyData && $host = $this->headers->get('X_FORWARDED_HOST')) { + if (self::$trustProxyData && self::$trustedHeaders['client_host'] && $host = $this->headers->get(self::$trustedHeaders['client_host'])) { $elements = explode(',', $host); $host = trim($elements[count($elements) - 1]); diff --git a/tests/Symfony/Tests/Component/HttpFoundation/RequestTest.php b/tests/Symfony/Tests/Component/HttpFoundation/RequestTest.php index c9b164581e5d..c6e11f05cd43 100644 --- a/tests/Symfony/Tests/Component/HttpFoundation/RequestTest.php +++ b/tests/Symfony/Tests/Component/HttpFoundation/RequestTest.php @@ -433,6 +433,9 @@ public function testGetHost() $request->initialize(array(), array(), array(), array(), array(), array('SERVER_NAME' => 'www.exemple.com')); $this->assertEquals('www.exemple.com', $request->getHost(), '->getHost() from 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 '); + Request::setTrustedProxies(array('1.1.1.1')); // X_FORWARDED_HOST @@ -453,10 +456,22 @@ public function testGetHost() $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 '); + // custom X_FORWARDED_HOST header name + Request::setTrustedHeaderName('client_host', 'X_MY_HOST'); + $request->initialize(array(), array(), array(), array(), array(), array('SERVER_NAME' => 'www.exemple.com', 'HTTP_X_MY_HOST' => 'www.forward.com')); + $this->assertEquals('www.forward.com', $request->getHost(), '->getHost() value from custom header name has priority over SERVER_NAME '); + + // X_FORWARDED_HOST ignored when custom header name is empty + Request::setTrustedHeaderName('client_host', null); + $request->initialize(array(), array(), array(), array(), array(), array('SERVER_NAME' => 'www.exemple.com', 'HTTP_X_FORWARDED_HOST' => 'www.forward.com')); + $this->assertEquals('www.exemple.com', $request->getHost(), '->getHost() value from X_FORWARDED_HOST has priority over SERVER_NAME '); + Request::setTrustedHeaderName('client_host', 'X_FORWARDED_HOST'); Request::setTrustedProxies(array()); + + // X_FORWARDED_HOST ignored when no trusted proxies + $request->initialize(array(), array(), array(), array(), array(), array('SERVER_NAME' => 'www.exemple.com', 'HTTP_X_FORWARDED_HOST' => 'www.forward.com')); + $this->assertEquals('www.exemple.com', $request->getHost(), '->getHost() value from X_FORWARDED_HOST has priority over SERVER_NAME '); } /** @@ -510,6 +525,8 @@ public function testGetClientIp($expected, $proxy, $remoteAddr, $httpForwardedFo $request->initialize(array(), array(), array(), array(), array(), $server); $this->assertEquals($expected, $request->getClientIp($proxy)); + + Request::setTrustedProxies(array()); } public function testGetClientIpProvider() @@ -619,7 +636,9 @@ public function testOverrideGlobals() $request->headers->set('X_FORWARDED_PROTO', 'https'); + Request::setTrustedProxies(array('1.1.1.1')); $this->assertTrue($request->isSecure()); + Request::setTrustedProxies(array()); $request->overrideGlobals(); @@ -813,12 +832,28 @@ public function testGetRequestFormat() public function testForwardedSecure() { - $request = new Request(); + $request = Request::create('http://test.com/'); $request->headers->set('X-Forwarded-Proto', 'https'); $request->headers->set('X-Forwarded-Port', 443); + $this->assertFalse($request->isSecure()); + $this->assertEquals(80, $request->getPort()); + + Request::setTrustedProxies(array('1.1.1.1')); + $this->assertTrue($request->isSecure()); $this->assertEquals(443, $request->getPort()); + + // custom header names + Request::setTrustedHeaderName('client_proto', 'X-My-Proto'); + Request::setTrustedHeaderName('client_port', 'X-My-Port'); + $request->headers->set('X-My-Proto', 'http'); + $request->headers->set('X-My-Port', 81); + + $this->assertFalse($request->isSecure()); + $this->assertEquals(81, $request->getPort()); + + Request::setTrustedProxies(array()); } public function testHasSession()