Skip to content

Commit 00a053e

Browse files
authored
Merge pull request #51 from akrabat/45-trust-from-the-right
Trust the first non-trusted-proxy IP address from the right
2 parents ee459cf + b6ba643 commit 00a053e

File tree

4 files changed

+187
-89
lines changed

4 files changed

+187
-89
lines changed

README.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,17 @@ If required, update your `.env` file with the environmental variables found in `
9595

9696
## Testing
9797

98-
* Code style: ``$ vendor/bin/phpcs``
99-
* Unit tests: ``$ vendor/bin/phpunit``
100-
* Code coverage: ``$ vendor/bin/phpunit --coverage-html ./build``
98+
* Code style: `$ vendor/bin/phpcs`
99+
* Fix style: `$ vendor/bin/phpcbf`
100+
* Unit tests: `$ vendor/bin/phpunit`
101+
* Code coverage: `$ vendor/bin/phpunit --coverage-html ./build`
102+
103+
You can also use Composer scripts:
104+
105+
* Check both: `$ composer check`
106+
* Code style: `$ composer cs`
107+
* Fix style: `$ composer cs-fix`
108+
* Unit tests: `$ composer test`
101109

102110

103111
[Master]: https://travis-ci.org/akrabat/ip-address-middleware

composer.json

+10
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,15 @@
3737
"laminas": {
3838
"config-provider": "RKA\\Middleware\\Mezzio\\ConfigProvider"
3939
}
40+
},
41+
"scripts": {
42+
"test": "phpunit",
43+
"cs": "phpcs",
44+
"cs-fix": "phpcbf",
45+
"code-coverage": "phpunit --coverage-html=coverage ./build",
46+
"check": [
47+
"@cs",
48+
"@test"
49+
]
4050
}
4151
}

src/IpAddress.php

+88-64
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class IpAddress implements MiddlewareInterface
2727
*
2828
* @var array
2929
*/
30-
protected $trustedProxies;
30+
protected $trustedProxies = [];
3131

3232
/**
3333
* List of trusted proxy IP wildcard ranges
@@ -83,7 +83,7 @@ public function __construct(
8383

8484
$this->checkProxyHeaders = $checkProxyHeaders;
8585

86-
if ($trustedProxies) {
86+
if (is_array($trustedProxies)) {
8787
foreach ($trustedProxies as $proxy) {
8888
if (strpos($proxy, '*') !== false) {
8989
// Wildcard IP address
@@ -175,54 +175,102 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res
175175
* @param ServerRequestInterface $request PSR-7 Request
176176
* @return string
177177
*/
178-
protected function determineClientIpAddress($request)
178+
protected function determineClientIpAddress($request): ?string
179179
{
180-
$ipAddress = '';
180+
$ipAddress = null;
181181

182182
$serverParams = $request->getServerParams();
183183
if (isset($serverParams['REMOTE_ADDR'])) {
184184
$remoteAddr = $this->extractIpAddress($serverParams['REMOTE_ADDR']);
185-
if ($this->isValidIpAddress($remoteAddr)) {
185+
if (filter_var($remoteAddr, FILTER_VALIDATE_IP)) {
186186
$ipAddress = $remoteAddr;
187187
}
188188
}
189+
if (!$this->checkProxyHeaders) {
190+
// do not check if configured to not check
191+
return $ipAddress;
192+
}
189193

190-
if ($this->shouldCheckProxyHeaders($ipAddress)) {
191-
foreach ($this->headersToInspect as $header) {
192-
if ($request->hasHeader($header)) {
193-
$ip = $this->getFirstIpAddressFromHeader($request, $header);
194-
if ($this->isValidIpAddress($ip)) {
195-
$ipAddress = $ip;
196-
break;
197-
}
194+
// If trustedProxies is empty, then the remote address is the trusted proxy
195+
$trustedProxies = $this->trustedProxies;
196+
if (empty($trustedProxies) && empty($this->trustedWildcards) && empty($this->trustedCidrs)) {
197+
$trustedProxies[] = $ipAddress;
198+
}
199+
200+
// find the first non-empty header from the headersToInspect list and use just that one
201+
foreach ($this->headersToInspect as $header) {
202+
if ($request->hasHeader($header)) {
203+
$headerValue = $request->getHeaderLine($header);
204+
if (!empty($headerValue)) {
205+
$ipAddress = $this->getIpAddressFromHeader(
206+
$header,
207+
$headerValue,
208+
$ipAddress,
209+
$trustedProxies
210+
);
211+
break;
198212
}
199213
}
200214
}
201215

202216
return empty($ipAddress) ? null : $ipAddress;
203217
}
204218

205-
/**
206-
* Determine whether we should check proxy headers for specified ip address
207-
*/
208-
protected function shouldCheckProxyHeaders(string $ipAddress): bool
209-
{
210-
//do not check if configured to not check
211-
if (!$this->checkProxyHeaders) {
212-
return false;
219+
public function getIpAddressFromHeader(
220+
string $headerName,
221+
string $headerValue,
222+
string $ipAddress,
223+
array $trustedProxies
224+
) {
225+
if (strtolower($headerName) == 'forwarded') {
226+
// The Forwarded header is different, so we need to extract the for= values. Note that we perform a
227+
// simple extraction here, and do not support the full RFC 7239 specification.
228+
preg_match_all('/for=([^,;]+)/i', $headerValue, $matches);
229+
$ipList = $matches[1];
230+
231+
// If any of the items in the list are not an IP address, then we ignore the entire list for now
232+
foreach ($ipList as $ip) {
233+
$ip = $this->extractIpAddress($ip);
234+
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
235+
return $ipAddress;
236+
}
237+
}
238+
} else {
239+
$ipList = explode(',', $headerValue);
213240
}
241+
$ipList[] = $ipAddress;
214242

215-
//if configured to check but no constraints
216-
if (!$this->trustedProxies && !$this->trustedWildcards && !$this->trustedCidrs) {
217-
return true;
243+
// Remove port from each item in the list
244+
$ipList = array_map(function ($ip) {
245+
return $this->extractIpAddress(trim($ip));
246+
}, $ipList);
247+
248+
// Ensure all IPs are valid and return $ipAddress if not
249+
foreach ($ipList as $ip) {
250+
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
251+
return $ipAddress;
252+
}
218253
}
219254

220-
// Exact Match for trusted proxies
221-
if ($this->trustedProxies && in_array($ipAddress, $this->trustedProxies)) {
255+
// walk list from right to left removing known proxy IP addresses.
256+
$ipList = array_reverse($ipList);
257+
foreach ($ipList as $ip) {
258+
$ip = trim($ip);
259+
if (!empty($ip) && !$this->isTrustedProxy($ip, $trustedProxies)) {
260+
return $ip;
261+
}
262+
}
263+
264+
return $ipAddress;
265+
}
266+
267+
protected function isTrustedProxy(string $ipAddress, array $trustedProxies): bool
268+
{
269+
if (in_array($ipAddress, $trustedProxies)) {
222270
return true;
223271
}
224272

225-
// Wildcard Match
273+
// Do we match a wildcard?
226274
if ($this->trustedWildcards) {
227275
// IPv4 has 4 parts separated by '.'
228276
// IPv6 has 8 parts separated by ':'
@@ -252,7 +300,7 @@ protected function shouldCheckProxyHeaders(string $ipAddress): bool
252300
}
253301
}
254302

255-
// CIDR Match
303+
// Do we match a CIDR address?
256304
if ($this->trustedCidrs) {
257305
// Only IPv4 is supported for CIDR matching
258306
$ipAsLong = ip2long($ipAddress);
@@ -265,7 +313,6 @@ protected function shouldCheckProxyHeaders(string $ipAddress): bool
265313
}
266314
}
267315

268-
//default - not check
269316
return false;
270317
}
271318

@@ -280,48 +327,25 @@ protected function shouldCheckProxyHeaders(string $ipAddress): bool
280327
protected function extractIpAddress($ipAddress)
281328
{
282329
$parts = explode(':', $ipAddress);
330+
if (count($parts) == 1) {
331+
return $ipAddress;
332+
}
283333
if (count($parts) == 2) {
284334
if (filter_var($parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false) {
285335
return $parts[0];
286336
}
287337
}
288338

289-
return $ipAddress;
290-
}
291-
292-
/**
293-
* Check that a given string is a valid IP address
294-
*
295-
* @param string $ip
296-
* @return boolean
297-
*/
298-
protected function isValidIpAddress(string $ip): bool
299-
{
300-
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) !== false;
301-
}
302-
303-
/**
304-
* Find out the client's IP address from the headers available to us
305-
*
306-
* @param ServerRequestInterface $request PSR-7 Request
307-
* @param string $header Header name
308-
* @return string
309-
*/
310-
private function getFirstIpAddressFromHeader(MessageInterface $request, string $header): string
311-
{
312-
$items = explode(',', $request->getHeaderLine($header));
313-
$headerValue = trim(reset($items));
314-
315-
if (ucfirst($header) == 'Forwarded') {
316-
foreach (explode(';', $headerValue) as $headerPart) {
317-
if (strtolower(substr($headerPart, 0, 4)) == 'for=') {
318-
$for = explode(']', $headerPart);
319-
$headerValue = trim(substr(reset($for), 4), " \t\n\r\0\x0B" . "\"[]");
320-
break;
321-
}
322-
}
339+
// If the $ipAddress starts with a [ and ends with ] or ]:port, then it is an IPv6 address and
340+
// we can extract the IP address
341+
$ipAddress = trim($ipAddress, '"\'');
342+
if (substr($ipAddress, 0, 1) === '['
343+
&& (substr($ipAddress, -1) === ']' || preg_match('/\]:\d+$/', $ipAddress))) {
344+
// Extract IPv6 address between brackets
345+
preg_match('/\[(.*?)\]/', $ipAddress, $matches);
346+
$ipAddress = $matches[1];
323347
}
324348

325-
return $this->extractIpAddress($headerValue);
349+
return $ipAddress;
326350
}
327351
}

0 commit comments

Comments
 (0)