Skip to content

Commit 3595b64

Browse files
committed
Select the right most IP address
Any IP addresses to the left after the known proxies are irrelevant and could have been spoofed. Therefore we need to select the first IP address from the right that is not a trusted proxy.
1 parent c865cfd commit 3595b64

File tree

1 file changed

+88
-64
lines changed

1 file changed

+88
-64
lines changed

src/IpAddress.php

Lines changed: 88 additions & 64 deletions
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)