Get client's real IP address on Heroku

caw picture caw · Aug 16, 2013 · Viewed 20.3k times · Source

On Heroku Cedar, I wanted to get the client's IP. First attempt:

ENV['REMOTE_ADDR']

This does not work, of course, because all requests are passed through proxies. So the alternative was to use:

ENV['HTTP_X_FORWARDED_FOR']

But this is not quite safe, is it?

If it contains only one value, I take this. If it contains more than one value (comma-separated), I could take the first one.

But what if someone manipulates this value? I cannot trust ENV['HTTP_X_FORWARDED_FOR'] as I could with ENV['REMOTE_ADDR']. And there is no list of trusted proxies that I could use, either.

But there must be some way to reliably get the client's IP address, always. Do you know one?

In their docs, Heroku describes that X-Forwarded-For is "the originating IP address of the client connecting to the Heroku router".

This sounds as if Heroku could be overwriting the X-Forwarded-For with the originating remote IP. This would prevent spoofing, right? Can someone verify this?

Answer

caw picture caw · Aug 29, 2013

From Jacob, Heroku's Director of Security at the time:

The router doesn't overwrite X-Forwarded-For, but it does guarantee that the real origin will always be the last item in the list.

This means that, if you access a Heroku app in the normal way, you will just see your IP address in the X-Forwarded-For header:

$ curl http://httpbin.org/ip
{
  "origin": "123.124.125.126",
}

If you try to spoof the IP, your alleged origin is reflected, but - critically - so is your real IP. Obviously, this is all we need, so there's a clear and secure solution for getting the client's IP address on Heroku:

$ curl -H"X-Forwarded-For: 8.8.8.8" http://httpbin.org/ip
{
  "origin": "8.8.8.8, 123.124.125.126"
}

This is just the opposite of what is described on Wikipedia, by the way.

PHP implementation:

function getIpAddress() {
    if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $ipAddresses = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        return trim(end($ipAddresses));
    }
    else {
        return $_SERVER['REMOTE_ADDR'];
    }
}