Skip to content

Commit e8ea2cd

Browse files
authored
Merge pull request #54 from akrabat/hop-count
Add hop count parameter
2 parents 20ecdc3 + 9a1c050 commit e8ea2cd

File tree

3 files changed

+95
-15
lines changed

3 files changed

+95
-15
lines changed

README.md

+11-4
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@ composer require akrabat/ip-address-middleware
1212

1313
## Configuration
1414

15-
The constructor takes 4 parameters which can be used to configure this middleware.
15+
The constructor takes 5 parameters which can be used to configure this middleware.
1616

1717
**Check proxy headers**
1818

19-
Note that the proxy headers are only checked if the first parameter to the constructor is set to `true`. If it is set to `false`, then only `$_SERVER['REMOTE_ADDR']` is used.
19+
The proxy headers are only checked if the first parameter to the constructor is set to `true`. If it is set to `false`, then only `$_SERVER['REMOTE_ADDR']` is used.
2020

2121
**Trusted Proxies**
2222

23-
If you configure to check the proxy headers (first parameter is `true`), you have to provide an array of trusted proxies as the second parameter. When the array is empty, the proxy headers will always be evaluated which is not recommended. If the array is not empty, it must contain strings with IP addresses (wildcard `*` is allowed in any given part) or networks in CIDR-notation. One of them must match the `$_SERVER['REMOTE_ADDR']` variable in order to allow evaluating the proxy headers - otherwise the `REMOTE_ADDR` itself is returned.
23+
If you enable checking of the proxy headers (first parameter is `true`), you have to provide an array as the second parameter. This is the list of IP addresses (supporting wildcards) of your proxy servers. If the array is empty, the proxy headers will always be used and the selection is based on the hop count (parameter 5).
24+
25+
If the array is not empty, it must contain strings with IP addresses (wildcard `*` is allowed in any given part) or networks in CIDR-notation. One of them must match the `$_SERVER['REMOTE_ADDR']` variable in order to allow evaluating the proxy headers - otherwise the `REMOTE_ADDR` itself is returned. This list is not ordered and there is no requirement that any given proxy header includes all the listed proxies.
2426

2527
**Attribute name**
2628

@@ -56,6 +58,11 @@ If you use _CloudFlare_, then according to the [documentation][cloudflare] you s
5658
[nginx]: http://nginx.org/en/docs/http/ngx_http_realip_module.html
5759
[cloudflare]: https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-
5860

61+
**hop count**
62+
63+
Set this to the number of known proxies between ingress and the application. This is used to determine the number of
64+
proxies to check in the `X-Forwarded-For` header, and is generally used when the IP addresses of the proxies cannot
65+
be reliably determined. The default is 0.
5966

6067
## Security considerations
6168

@@ -71,7 +78,7 @@ In Mezzio, copy `Mezzio/config/ip_address.global.php.dist` into your Mezzio Appl
7178

7279
## Usage
7380

74-
In Slim 3:
81+
In Slim:
7582

7683
```php
7784
$checkProxyHeaders = true; // Note: Never trust the IP address for security processes!

src/IpAddress.php

+32-11
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ class IpAddress implements MiddlewareInterface
5050
*/
5151
protected $attributeName = 'ip_address';
5252

53+
/**
54+
* Number of hops that can be considered safe. Set to a positive number to enable.
55+
*
56+
* @var int
57+
*/
58+
protected $hopCount = 0;
59+
5360
/**
5461
* List of proxy headers inspected for the client IP address
5562
*
@@ -67,15 +74,17 @@ class IpAddress implements MiddlewareInterface
6774
* Constructor
6875
*
6976
* @param bool $checkProxyHeaders Whether to use proxy headers to determine client IP
70-
* @param array $trustedProxies List of IP addresses of trusted proxies
77+
* @param ?array $trustedProxies Unordered list of IP addresses of trusted proxies
7178
* @param string $attributeName Name of attribute added to ServerRequest object
7279
* @param array $headersToInspect List of headers to inspect
80+
* @param int $hopCount Number of hops that can be considered safe. Set to a positive number to enable
7381
*/
7482
public function __construct(
7583
$checkProxyHeaders = false,
7684
?array $trustedProxies = null,
7785
$attributeName = null,
78-
array $headersToInspect = []
86+
array $headersToInspect = [],
87+
int $hopCount = 0
7988
) {
8089
if ($checkProxyHeaders && $trustedProxies === null) {
8190
throw new \InvalidArgumentException('Use of the forward headers requires an array for trusted proxies.');
@@ -105,6 +114,8 @@ public function __construct(
105114
if (!empty($headersToInspect)) {
106115
$this->headersToInspect = $headersToInspect;
107116
}
117+
118+
$this->hopCount = $hopCount;
108119
}
109120

110121
private function parseWildcard(string $ipAddress): array
@@ -211,7 +222,8 @@ protected function determineClientIpAddress($request): ?string
211222
$header,
212223
$headerValue,
213224
$ipAddress,
214-
$trustedProxies
225+
$trustedProxies,
226+
$this->hopCount
215227
);
216228
break;
217229
}
@@ -224,8 +236,9 @@ protected function determineClientIpAddress($request): ?string
224236
public function getIpAddressFromHeader(
225237
string $headerName,
226238
string $headerValue,
227-
string $ipAddress,
228-
array $trustedProxies
239+
string $thisIpAddress,
240+
array $trustedProxies,
241+
int $hopCount
229242
) {
230243
if (strtolower($headerName) == 'forwarded') {
231244
// The Forwarded header is different, so we need to extract the for= values. Note that we perform a
@@ -237,13 +250,13 @@ public function getIpAddressFromHeader(
237250
foreach ($ipList as $ip) {
238251
$ip = $this->extractIpAddress($ip);
239252
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
240-
return $ipAddress;
253+
return $thisIpAddress;
241254
}
242255
}
243256
} else {
244257
$ipList = explode(',', $headerValue);
245258
}
246-
$ipList[] = $ipAddress;
259+
$ipList[] = $thisIpAddress;
247260

248261
// Remove port from each item in the list
249262
$ipList = array_map(function ($ip) {
@@ -253,20 +266,28 @@ public function getIpAddressFromHeader(
253266
// Ensure all IPs are valid and return $ipAddress if not
254267
foreach ($ipList as $ip) {
255268
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
256-
return $ipAddress;
269+
return $thisIpAddress;
257270
}
258271
}
259272

260273
// walk list from right to left removing known proxy IP addresses.
261274
$ipList = array_reverse($ipList);
275+
$count = 0;
262276
foreach ($ipList as $ip) {
263-
$ip = trim($ip);
264-
if (!empty($ip) && !$this->isTrustedProxy($ip, $trustedProxies)) {
277+
$count++;
278+
if (!$this->isTrustedProxy($ip, $trustedProxies)) {
279+
if ($count <= $hopCount) {
280+
continue;
281+
}
265282
return $ip;
283+
// } else {
284+
// if ($count <= $hopCount) {
285+
// continue;
286+
// }
266287
}
267288
}
268289

269-
return $ipAddress;
290+
return $thisIpAddress;
270291
}
271292

272293
protected function isTrustedProxy(string $ipAddress, array $trustedProxies): bool

tests/IpAddressTest.php

+52
Original file line numberDiff line numberDiff line change
@@ -433,4 +433,56 @@ public function testThatATrustedProxiesInWrongPlaceIsIgnored()
433433

434434
$this->assertSame('192.168.1.2', $ipAddress);
435435
}
436+
437+
/**
438+
* If the hop count is set, then it is used to determine the IP address to return
439+
*/
440+
public function testHopCountIsUsedWhenNoTrustedProxiesAreDefined()
441+
{
442+
$middleware = new IPAddress(true, [], null, [], 3);
443+
$env = [
444+
'REMOTE_ADDR' => '192.168.1.1',
445+
'HTTP_X_FORWARDED_FOR' => '192.168.1.5, 192.168.1.4, 192.168.1.3, 192.168.1.2'
446+
];
447+
$ipAddress = $this->simpleRequest($middleware, $env);
448+
449+
// With three trusted hops, the 4th IP address should be found
450+
$this->assertSame('192.168.1.4', $ipAddress);
451+
}
452+
453+
/**
454+
* With the hop count set, the IP address returned is the first IP address after the hop count even
455+
* if there are non-trusted IP addresses before it in the list.
456+
*/
457+
public function testHopCountOverridesTrustedProxies()
458+
{
459+
$middleware = new IPAddress(true, ['192.168.1.2', '192.168.1.1'], null, [], 3);
460+
$env = [
461+
'REMOTE_ADDR' => '192.168.1.1',
462+
'HTTP_X_FORWARDED_FOR' => '192.168.1.5, 192.168.1.4, 192.168.1.3, 192.168.1.2'
463+
];
464+
$ipAddress = $this->simpleRequest($middleware, $env);
465+
466+
// With three trusted hops, the 4th IP address should be found even though the third IP address
467+
// is not a trusted proxy
468+
$this->assertSame('192.168.1.4', $ipAddress);
469+
}
470+
471+
/**
472+
* With the hop count is set, if the IP address at the hop count is a trusted proxy, then
473+
* select the first IP address after it that is not a trusted proxy
474+
*/
475+
public function testHopCountDoesNotReturnATrustedProxy()
476+
{
477+
$middleware = new IPAddress(true, ['192.168.1.2', '192.168.1.1'], null, [], 1);
478+
$env = [
479+
'REMOTE_ADDR' => '192.168.1.1',
480+
'HTTP_X_FORWARDED_FOR' => '192.168.1.5, 192.168.1.4, 192.168.1.3, 192.168.1.2'
481+
];
482+
$ipAddress = $this->simpleRequest($middleware, $env);
483+
484+
// With 1 trusted hop, the second IP address would be found, except that it is a trusted proxy
485+
// itself, so the third IP address should be found
486+
$this->assertSame('192.168.1.3', $ipAddress);
487+
}
436488
}

0 commit comments

Comments
 (0)