Skip to content

Commit

Permalink
Add '**' as an option for proxies, to trust any IP address (#46, #49)
Browse files Browse the repository at this point in the history
However, if you are in the situation where, say, you have a
Content Distribution Network (like Amazon CloudFront) that passes to load
balancer (like Amazon ELB) then you may end up with a chain of unknown
proxies forwarding from one to another. In that case, '*' above would
only match the final proxy (the load balancer in this case) which means
that calling `$request->getClientIp()` would return the IP address
of the next proxy in line (in this case one of the Content Distribution
Network ips) rather than the original client IP.To always get the original
client IP, you need to trust all the proxies in the route to your request.

You can do this by setting proxies to ['0.0.0.0/0', '2000:0:0:0:0:0:0:0/3']
which means all IP addresses will be trusted.

This patch implements ** as syntactic sugar for ['0.0.0.0/0', '2000:0:0:0:0:0:0:0/3']
  • Loading branch information
tamc committed Dec 1, 2016
1 parent 764b0b0 commit 6018dfb
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 38 deletions.
24 changes: 21 additions & 3 deletions config/trustedproxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,37 @@
* supported, along with CIDR notation.
*
* The "*" character is syntactic sugar
* within TrustedProxy to trust any proxy;
* within TrustedProxy to trust any proxy
* that connects directly to your server,
* a requirement when you cannot know the address
* of your proxy (e.g. if using Rackspace balancers).
*
* The "**" character is syntactic sugar within
* TrustedProxy to trust not just any proxy that
* connects directly to your server, but also
* proxies that connect to those proxies, and all
* the way back until you reach the original source
* IP. It will mean that $request->getClientIp()
* always gets the originating client IP, no matter
* how many proxies that client's request has
* subsequently passed through.
*/
'proxies' => [
'192.168.1.10',
],

/*
* Or, to trust all proxies, uncomment this:
* Or, to trust all proxies that connect
* directly to your server, uncomment this:
*/
# 'proxies' => '*',

/*
* Or, to trust ALL proxies, including those that
* are in a chain of fowarding, uncomment this:
*/
# 'proxies' => '**',

/*
* Default Header Names
*
Expand All @@ -40,4 +58,4 @@
\Illuminate\Http\Request::HEADER_CLIENT_PROTO => 'X_FORWARDED_PROTO',
\Illuminate\Http\Request::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT',
]
];
];
51 changes: 45 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,19 +181,37 @@ return [
* supported, along with CIDR notation.
*
* The "*" character is syntactic sugar
* within TrustedProxy to trust any proxy;
* within TrustedProxy to trust any proxy
* that connects directly to your server,
* a requirement when you cannot know the address
* of your proxy (e.g. if using Rackspace balancers).
*
* The "**" character is syntactic sugar within
* TrustedProxy to trust not just any proxy that
* connects directly to your server, but also
* proxies that connect to those proxies, and all
* the way back until you reach the original source
* IP. It will mean that $request->getClientIp()
* always gets the originating client IP, no matter
* how many proxies that client's request has
* subsequently passed through.
*/
'proxies' => [
'192.168.1.10',
],

/*
* Or, to trust all proxies, uncomment this:
* Or, to trust all proxies that connect
* directly to your server, uncomment this:
*/
# 'proxies' => '*',

/*
* Or, to trust ALL proxies, including those that
* are in a chain of fowarding, uncomment this:
*/
# 'proxies' => '**',

/*
* Default Header Names
*
Expand All @@ -207,10 +225,10 @@ return [
* \Symfony\Component\HttpFoundation\Request::$trustedHeaders
*/
'headers' => [
Illuminate\Http\Request::HEADER_CLIENT_IP => 'X_FORWARDED_FOR',
Illuminate\Http\Request::HEADER_CLIENT_HOST => 'X_FORWARDED_HOST',
Illuminate\Http\Request::HEADER_CLIENT_PROTO => 'X_FORWARDED_PROTO',
Illuminate\Http\Request::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT',
\Illuminate\Http\Request::HEADER_CLIENT_IP => 'X_FORWARDED_FOR',
\Illuminate\Http\Request::HEADER_CLIENT_HOST => 'X_FORWARDED_HOST',
\Illuminate\Http\Request::HEADER_CLIENT_PROTO => 'X_FORWARDED_PROTO',
\Illuminate\Http\Request::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT',
]
];
```
Expand All @@ -233,6 +251,27 @@ return [

Using `*` will tell Laravel to trust all IP addresses as a proxy.

However, if you are in the situation where, say, you have a Content Distribution Network (like Amazon CloudFront) that passes to load balancer (like Amazon ELB)
then you may end up with a chain of unknown proxies forwarding from one to another. In that case, '*' above would only match
the final proxy (the load balancer in this case) which means that calling `$request->getClientIp()` would return the IP address
of the next proxy in line (in this case one of the Content Distribution Network ips) rather than the original client IP.
To always get the original client IP, you need to trust all the proxies in the route to your request. You can do this by:

**In that case, you can set the 'proxies' variable to '**':**

```php
<?php

return [

'proxies' => '**',

];
```

Which will trust every single IP address.


#### Changing X-Forwarded-* Header Names

By default, the underlying Symfony `Request` class expects the following header names to be sent from a proxy:
Expand Down
74 changes: 45 additions & 29 deletions src/TrustProxies.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,52 +36,68 @@ public function __construct(Repository $config)
*/
public function handle($request, Closure $next)
{
// Set trusted header names
foreach ($this->getTrustedHeaders() as $headerKey => $headerName) {
$request->setTrustedHeaderName($headerKey, $headerName);
}

$request->setTrustedProxies($this->getTrustedProxies($request->getClientIps()));

$this->setTrustedProxyHeaderNames($request);
$this->setTrustedProxyIpAddresses($request);
return $next($request);
}

/**
* Return an array of trusted proxy IP addresses.
* Sets the trusted proxies on the request to the value of trustedproxy.proxies
*
* @param array $clientIpAddresses Array of client IP addresses retrieved
* *prior* to setting trusted proxy
*
* @return array
* @param \Illuminate\Http\Request $request
*/
protected function getTrustedProxies(array $clientIpAddresses = [])
protected function setTrustedProxyIpAddresses($request)
{
$trustedProxies = $this->config->get('trustedproxy.proxies');
$trustedIps = $this->config->get('trustedproxy.proxies');

// We only trust specific IP addresses
if(is_array($trustedIps)) {
return $this->setTrustedProxyIpAddressesToSpecificIps($request, $trustedIps);
}

// To trust all proxies, we set trusted proxies to all IP addresses.
if ($trustedProxies === '*') {
return $clientIpAddresses;
// We trust any IP address that calls us, but not proxies further
// up the forwarding chain.
if ($trustedIps === '*') {
return $this->setTrustedProxyIpAddressesToTheCallingIp($request);
}

// We trust all proxies. Those that call us, and those that are
// further up the calling chain (e.g., where the X-FORWARDED-FOR
// header has multiple IP addresses listed);
if ($trustedIps === '**') {
return $this->setTrustedProxyIpAddressesToAllIps($request);
}
}

return (array) $trustedProxies;
private function setTrustedProxyIpAddressesToSpecificIps($request, $trustedIps)
{
$request->setTrustedProxies((array) $trustedIps);
}

private function setTrustedProxyIpAddressesToTheCallingIp($request) {
$request->setTrustedProxies($request->getClientIps());
}

private function setTrustedProxyIpAddressesToAllIps($request)
{
// 0.0.0.0/0 is the CIDR for all ipv4 addresses
// 2000:0:0:0:0:0:0:0/3 is the CIDR for all ipv6 addresses currently
// allocated http://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.xhtml
$request->setTrustedProxies(['0.0.0.0/0', '2000:0:0:0:0:0:0:0/3']);
}

/**
* Get trusted header names.
* Set the trusted header names based on teh content of trustedproxy.headers
*
* @return array
* @param \Illuminate\Http\Request $request
*/
protected function getTrustedHeaders()
protected function setTrustedProxyHeaderNames($request)
{
$trustedHeaderNames = $this->config->get('trustedproxy.headers');
if(!is_array($trustedHeaderNames)) { return; } // Leave the defaults

/*
* In case the user does not pass an array of header names we
* will default to an empty array. This will force defaults from
* class \Symfony\Component\HttpFoundation\Request::$trustedHeaders
*/
$trustedHeaderNames = is_array($trustedHeaderNames) ? $trustedHeaderNames : [];

return $trustedHeaderNames;
foreach ($trustedHeaderNames as $headerKey => $headerName) {
$request->setTrustedHeaderName($headerKey, $headerName);
}
}
}
64 changes: 64 additions & 0 deletions tests/TrustedProxyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ public function test_does_trust_trusted_proxy()
$this->assertEquals(443, $req->getPort(), 'Assert trusted proxy x-forwarded-port header used');
}

/**
* Test the next most typical usage of TrustedProxies:
* Trusted X-Forwarded-For header, wilcard for TrustedProxies
*/
public function test_trusted_proxy_sets_trusted_proxies_with_wildcard()
{
$trustedProxy = $this->createTrustedProxy([], '*');
$request = $this->createProxiedRequest();

$trustedProxy->handle($request, function ($request) {
$this->assertEquals('173.174.200.38', $request->getClientIp(), 'Assert trusted proxy x-forwarded-for header used with wildcard proxy setting');
});
}



/**
* Test the most typical usage of TrustProxies:
* Trusted X-Forwarded-For header
Expand Down Expand Up @@ -102,6 +118,54 @@ public function test_get_client_ip_with_muliple_ip_addresses_some_of_which_are_t
}
}

/**
* Test X-Forwarded-For header with multiple IP addresses, with * wildcard trusting of all proxies
*/
public function test_get_client_ip_with_muliple_ip_addresses_all_proxies_are_trusted()
{
$trustedProxy = $this->createTrustedProxy([], '*');

$forwardedFor = [
'192.0.2.2',
'192.0.2.199, 192.0.2.2',
'192.0.2.199,192.0.2.2',
'99.99.99.99,192.0.2.199,192.0.2.2',
];

foreach($forwardedFor as $forwardedForHeader) {
$request = $this->createProxiedRequest(['HTTP_X_FORWARDED_FOR' => $forwardedForHeader]);

$trustedProxy->handle($request, function ($request) use ($forwardedForHeader) {
$this->assertEquals('192.0.2.2', $request->getClientIp(), 'Assert sets the '.$forwardedForHeader);
});
}
}

/**
* Test X-Forwarded-For header with multiple IP addresses, with ** wildcard trusting of all proxies in the chain
*/
public function test_get_client_ip_with_muliple_ip_addresses_all_proxies_and_all_forwarding_proxies_are_trusted()
{
$trustedProxy = $this->createTrustedProxy([], '**');

$forwardedFor = [
'192.0.2.2',
'192.0.2.2, 192.0.2.199',
'192.0.2.2, 99.99.99.99, 192.0.2.199',
'192.0.2.2, 2001:0db8:0a0b:12f0:0000:0000:0000:0001, 192.0.2.199',
'192.0.2.2, 2c01:0db8:0a0b:12f0:0000:0000:0000:0001, 192.0.2.199',
'192.0.2.2,192.0.2.199',
];

foreach($forwardedFor as $forwardedForHeader) {
$request = $this->createProxiedRequest(['HTTP_X_FORWARDED_FOR' => $forwardedForHeader]);

$trustedProxy->handle($request, function ($request) use ($forwardedForHeader) {
$this->assertEquals('192.0.2.2', $request->getClientIp(), 'Assert sets the '.$forwardedForHeader);
});
}
}

/**
* Test renaming the X-Forwarded-For header.
*/
Expand Down

0 comments on commit 6018dfb

Please sign in to comment.