Skip to content

Added new Forwarded header support for Request::getClientIps #11379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions src/Symfony/Component/HttpFoundation/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
*/
class Request
{
const HEADER_FORWARDED = 'forwarded';
const HEADER_CLIENT_IP = 'client_ip';
const HEADER_CLIENT_HOST = 'client_host';
const HEADER_CLIENT_PROTO = 'client_proto';
Expand All @@ -46,6 +47,9 @@ class Request
const METHOD_TRACE = 'TRACE';
const METHOD_CONNECT = 'CONNECT';

/**
* @var string[]
*/
protected static $trustedProxies = array();

/**
Expand All @@ -62,10 +66,13 @@ class Request
* Names for headers that can be trusted when
* using trusted proxies.
*
* The default names are non-standard, but widely used
* The FORWARDED header is the standard as of rfc7239.
*
* The other headers are non-standard, but widely used
* by popular reverse proxies (like Apache mod_proxy or Amazon EC2).
*/
protected static $trustedHeaders = array(
self::HEADER_FORWARDED => 'FORWARDED',
self::HEADER_CLIENT_IP => 'X_FORWARDED_FOR',
self::HEADER_CLIENT_HOST => 'X_FORWARDED_HOST',
self::HEADER_CLIENT_PROTO => 'X_FORWARDED_PROTO',
Expand Down Expand Up @@ -823,24 +830,26 @@ public function setSession(SessionInterface $session)
*/
public function getClientIps()
{
$clientIps = array();
$ip = $this->server->get('REMOTE_ADDR');

if (!self::$trustedProxies) {
return array($ip);
}

if (!self::$trustedHeaders[self::HEADER_CLIENT_IP] || !$this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP])) {
return array($ip);
if (self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) {
$forwardedHeader = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
preg_match_all('{(for)=("?\[?)([a-z0-9\.:_\-/]*)}', $forwardedHeader, $matches);
$clientIps = $matches[3];
} elseif (self::$trustedHeaders[self::HEADER_CLIENT_IP] && $this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP])) {
$clientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP])));
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexandresalome Concerning your previous comment, I am still checking the trusted headers here. Or am I missing something?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your first test is to verify if headers contains Forwarded, but you should only trust it if the IP is a Proxy IP.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexandresalome I now check the header is part of the self::$trustedHeaders, that should be right?

}

$clientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP])));
$clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from

$ip = $clientIps[0]; // Fallback to this when the client IP falls into the range of trusted proxies

// Eliminate all IPs from the forwarded IP chain which are trusted proxies
foreach ($clientIps as $key => $clientIp) {
// Remove port on IPv4 address (unfortunately, it does happen)
// Remove port (unfortunately, it does happen)
if (preg_match('{((?:\d+\.){3}\d+)\:\d+}', $clientIp, $match)) {
$clientIps[$key] = $clientIp = $match[1];
}
Expand Down
44 changes: 44 additions & 0 deletions src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,31 @@ public function testGetClientIps($expected, $remoteAddr, $httpForwardedFor, $tru
Request::setTrustedProxies(array());
}

/**
* @dataProvider testGetClientIpsForwardedProvider
*/
public function testGetClientIpsForwarded($expected, $remoteAddr, $httpForwarded, $trustedProxies)
{
$request = $this->getRequestInstanceForClientIpsForwardedTests($remoteAddr, $httpForwarded, $trustedProxies);

$this->assertEquals($expected, $request->getClientIps());

Request::setTrustedProxies(array());
}

public function testGetClientIpsForwardedProvider()
{
// $expected $remoteAddr $httpForwarded $trustedProxies
return array(
array(array('127.0.0.1'), '127.0.0.1', 'for="_gazonk"', null),
array(array('_gazonk'), '127.0.0.1', 'for="_gazonk"', array('127.0.0.1')),
array(array('88.88.88.88'), '127.0.0.1', 'for="88.88.88.88:80"', array('127.0.0.1')),
array(array('192.0.2.60'), '::1', 'for=192.0.2.60;proto=http;by=203.0.113.43', array('::1')),
array(array('2620:0:1cfe:face:b00c::3', '192.0.2.43'), '::1', 'for=192.0.2.43, for=2620:0:1cfe:face:b00c::3', array('::1')),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth adding a test case for Forwarded: For="[2001:db8:cafe::17]:4711" returning [2001:db8:cafe::17]:4711. I don't think the current code handles it well. I also think it should preserve the [] for IPv6 as that is the common notation, but not sure if there are other precedents in symfony.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also worth adding a test for Forwarded: For="127.0.0.1:80" which should return 127.0.0.1:80 (I think it does, but just for completeness' sake)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've checked and it does not handle the port as is. I'll make the necessary changes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Seldaek I can't add a test for ips like [2001:db8:cafe::17]:4711 as it is later rejected by the IpUtils::checkIp call. What do you suggest?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tony-co [2001:db8:cafe::17]:4711 as header should return 2001:db8:cafe::17 for the IP, not [2001:db8:cafe::17], and it will be accepted. The square brackets are not part of the IP. They are only part of teh IP+port notation, because there are :in the IP itself

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sorry my initial comment was misleading I forgot that this method is called getClientIp while I was lost reading the spec :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping @stof Ok I added the test for [2001:db8:cafe::17]:4711. But still struggling with the 127.0.0.1:80 as the : misled the IpUtils::checkIp thinking this is an IPv6...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you should strip port numbers before passing them to checkIp, since this is just about getting the IP out it makes no sense to keep port info anyway.

array(array('2001:db8:cafe::17'), '::1', 'for="[2001:db8:cafe::17]:4711', array('::1')),
);
}

public function testGetClientIpsProvider()
{
// $expected $remoteAddr $httpForwardedFor $trustedProxies
Expand Down Expand Up @@ -1467,6 +1492,25 @@ private function getRequestInstanceForClientIpTests($remoteAddr, $httpForwardedF
return $request;
}

private function getRequestInstanceForClientIpsForwardedTests($remoteAddr, $httpForwarded, $trustedProxies)
{
$request = new Request();

$server = array('REMOTE_ADDR' => $remoteAddr);

if (null !== $httpForwarded) {
$server['HTTP_FORWARDED'] = $httpForwarded;
}

if ($trustedProxies) {
Request::setTrustedProxies($trustedProxies);
}

$request->initialize(array(), array(), array(), array(), array(), $server);

return $request;
}

public function testTrustedProxies()
{
$request = Request::create('http://example.com/');
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy