July 10, 2009

Real IP Problem and X-Forwarded-For header

Someone has been in a situation like this? I bet it

  • We need a way to know whether a user is logged in or not.
  • Easy as pie. Go on sessions with him.
  • We can't rely on the user's browser to allow cookies.
  • Well, we rely on the user's browser to have JavaScript, so why...?
  • I said no cookies. Use the IP address.

Then the system went into production, users connected to it, they paid for the services and user's IP were logged into the server and into the database, being checked in every request he made to the server, until the server logged them out. Not perfect, you may think, and you're right, because one day:

  • We've been receiving many tickets from our users asking they want their money back
  • Why?
  • They paid for a service they never were able to enter.
  • And no error was shown? Was all in the billing system OK?
  • No error. No problem. But they were not allowed to enter the site
  • I must investigate

And later on, after mailing nearly every customer and checking every log and pinging every IP, the problem arises: Proxies.

Proxies acts between client and server in order to speed the user experience on the web. Proxies can cache, redirect to nearby mirrors, filter content, provide anonymity and intranet connectivity, and so forth.

Proxies and the X-Forwarded-For problem

Proxies adds a X-Forwarded-For header to the request (or updates an existing one) with the user IP and, then, after some more magic, redirects the request to the destination. Answer goes also from the server to the proxy, then it's redirected to the real end user.

So, when a request comes directly from users, 'REMOTE_ADDR' PHP var contains the user's real IP. While browsing through a proxy, the 'REMOTE_ADDR' is the proxy's IP and the 'HTTP_X_FORWARDED_FOR' contains a list of one or more IP's, one of them being the user's one.

So you make a script to get the client's real IP, like this one:

function get_real_ip(){
  return isset($_SERVER['HTTP_X_FORWARDED_FOR'])
           ? $_SERVER['HTTP_X_FORWARDED_FOR']
           : $_SERVER['REMOTE_ADDR'];
}

Not a really good one, it can be improved by filtering IPs from the forwarded list and selecting only one, but, nevertheless, all version suffer the same problem: Header injection.

What happens if somebody changes (and it's not that difficult to do it!) the headers sent to the browser?

Let's say you add a X-Forwarded-For header and set its value to some IP, maybe one you 'sniffed' from the site traffic. Now you're superseding another user! If this user buys some content, now you're allowed to get it too.

Anonymous browsing

Also, if the Proxy allows anonymity, your server will receive no X-Forwarded-For header thus confusing the proxy's IP with the real one. Now, every computer passing through this Proxy can access the private content, as long as one of them purchased it.

Conclusions

That's not a security hole. It's a crater as big as a full moon. So, while developing "security measures" think for yourself: Are these making the hole smaller or even bigger?