Skip to content

Commit bb0a120

Browse files
committed
Add hop count parameter
Support setting a hop count for cases when you know the number of proxies in front of your app, but the list of possible IP addresses is large.
1 parent 17943ec commit bb0a120

File tree

3 files changed

+92
-15
lines changed

3 files changed

+92
-15
lines changed

README.md

Lines changed: 11 additions & 4 deletions
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

Lines changed: 32 additions & 11 deletions
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
@@ -206,7 +217,8 @@ protected function determineClientIpAddress($request): ?string
206217
$header,
207218
$headerValue,
208219
$ipAddress,
209-
$trustedProxies
220+
$trustedProxies,
221+
$this->hopCount
210222
);
211223
break;
212224
}
@@ -219,8 +231,9 @@ protected function determineClientIpAddress($request): ?string
219231
public function getIpAddressFromHeader(
220232
string $headerName,
221233
string $headerValue,
222-
string $ipAddress,
223-
array $trustedProxies
234+
string $thisIpAddress,
235+
array $trustedProxies,
236+
int $hopCount
224237
) {
225238
if (strtolower($headerName) == 'forwarded') {
226239
// The Forwarded header is different, so we need to extract the for= values. Note that we perform a
@@ -232,13 +245,13 @@ public function getIpAddressFromHeader(
232245
foreach ($ipList as $ip) {
233246
$ip = $this->extractIpAddress($ip);
234247
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
235-
return $ipAddress;
248+
return $thisIpAddress;
236249
}
237250
}
238251
} else {
239252
$ipList = explode(',', $headerValue);
240253
}
241-
$ipList[] = $ipAddress;
254+
$ipList[] = $thisIpAddress;
242255

243256
// Remove port from each item in the list
244257
$ipList = array_map(function ($ip) {
@@ -248,20 +261,28 @@ public function getIpAddressFromHeader(
248261
// Ensure all IPs are valid and return $ipAddress if not
249262
foreach ($ipList as $ip) {
250263
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
251-
return $ipAddress;
264+
return $thisIpAddress;
252265
}
253266
}
254267

255268
// walk list from right to left removing known proxy IP addresses.
256269
$ipList = array_reverse($ipList);
270+
$count = 0;
257271
foreach ($ipList as $ip) {
258-
$ip = trim($ip);
259-
if (!empty($ip) && !$this->isTrustedProxy($ip, $trustedProxies)) {
272+
$count++;
273+
if (!$this->isTrustedProxy($ip, $trustedProxies)) {
274+
if ($count <= $hopCount) {
275+
continue;
276+
}
260277
return $ip;
278+
// } else {
279+
// if ($count <= $hopCount) {
280+
// continue;
281+
// }
261282
}
262283
}
263284

264-
return $ipAddress;
285+
return $thisIpAddress;
265286
}
266287

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

tests/IpAddressTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,4 +422,53 @@ public function testThatATrustedProxiesInWrongPlaceIsIgnored()
422422

423423
$this->assertSame('192.168.1.2', $ipAddress);
424424
}
425+
426+
public function testHopCountIsUsedWhenNoTrustedProxiesAreDefined()
427+
{
428+
$middleware = new IPAddress(true, [], null, [], 3);
429+
$env = [
430+
'REMOTE_ADDR' => '192.168.1.1',
431+
'HTTP_X_FORWARDED_FOR' => '192.168.1.5, 192.168.1.4, 192.168.1.3, 192.168.1.2'
432+
];
433+
$ipAddress = $this->simpleRequest($middleware, $env);
434+
435+
// With three trusted hops, the 4th IP address should be found
436+
$this->assertSame('192.168.1.4', $ipAddress);
437+
}
438+
439+
/**
440+
* With the hop count set, the IP address returned is the first IP address after the hop count even
441+
* if there are non-trusted IP addresses before it in the list.
442+
*/
443+
public function testHopCountOverridesTrustedProxies()
444+
{
445+
$middleware = new IPAddress(true, ['192.168.1.2', '192.168.1.1'], null, [], 3);
446+
$env = [
447+
'REMOTE_ADDR' => '192.168.1.1',
448+
'HTTP_X_FORWARDED_FOR' => '192.168.1.5, 192.168.1.4, 192.168.1.3, 192.168.1.2'
449+
];
450+
$ipAddress = $this->simpleRequest($middleware, $env);
451+
452+
// With three trusted hops, the 4th IP address should be found even though the third IP address
453+
// is not a trusted proxy
454+
$this->assertSame('192.168.1.4', $ipAddress);
455+
}
456+
457+
/**
458+
* With the hop count is set, if the IP address at the hop count is a trusted proxy, then
459+
* select the first IP address that is not a trusted proxy
460+
*/
461+
public function testHopCountDoesNotReturnATrustedProxy()
462+
{
463+
$middleware = new IPAddress(true, ['192.168.1.2', '192.168.1.1'], null, [], 1);
464+
$env = [
465+
'REMOTE_ADDR' => '192.168.1.1',
466+
'HTTP_X_FORWARDED_FOR' => '192.168.1.5, 192.168.1.4, 192.168.1.3, 192.168.1.2'
467+
];
468+
$ipAddress = $this->simpleRequest($middleware, $env);
469+
470+
// With 1 trusted hop, the second IP address would be found, except that it is a trusted proxy
471+
// itself, so the third IP address should be found
472+
$this->assertSame('192.168.1.3', $ipAddress);
473+
}
425474
}

0 commit comments

Comments
 (0)