6

I run security checks on a number of AJAX calls to see if the same IP requested that I have on record.

I used the following set of class functions to establish the IP (which can come via load balancers, hence the lengthly methodology.

    private function IPMask_Match ($network, $ip) {
      $ip_arr = explode('/', $network);
      if (count($ip_arr) < 2) {
        $ip_arr = array($ip_arr[0], null);
      }
      $network_long = ip2long($ip_arr[0]);
      $x = ip2long($ip_arr[1]);
      $mask =  long2ip($x) == $ip_arr[1] ? $x : 0xffffffff << (32 - $ip_arr[1]);
      $ip_long = ip2long($ip);
      return ($ip_long & $mask) == ($network_long & $mask);
    }


    private function IPCheck_RFC1918 ($IP) {
      $PrivateIP = false;
      if (!$PrivateIP) {
        $PrivateIP = $this->IPMask_Match('127.0.0.0/8', $IP);
      }
      if (!$PrivateIP) {
        $PrivateIP = $this->IPMask_Match('10.0.0.0/8', $IP);
      }
      if (!$PrivateIP) {
        $PrivateIP = $this->IPMask_Match('172.16.0.0/12', $IP);
      }
      if (!$PrivateIP) {
        $PrivateIP = $this->IPMask_Match('192.168.0.0/16', $IP);
      }
      return $PrivateIP;
    }


    public function getIP () {
      $UsesProxy = (!empty($_SERVER['HTTP_X_FORWARDED_FOR']) || !empty($_SERVER['HTTP_CLIENT_IP'])) ? true : false;
      if ($UsesProxy && !empty($_SERVER['HTTP_CLIENT_IP'])) {
        $UserIP = $_SERVER['HTTP_CLIENT_IP'];
      }
      elseif ($UsesProxy && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $UserIP = $_SERVER['HTTP_X_FORWARDED_FOR'];
        if (strstr($UserIP, ',')) {
          $UserIPArray = explode(',', $UserIP);
          foreach ($UserIPArray as $IPtoCheck) {
            if (!$this->IPCheck_RFC1918($IPtoCheck)) {
              $UserIP = $IPtoCheck;
              break;
            }
          }
          if ($UserIP == $_SERVER['HTTP_X_FORWARDED_FOR']) {
            $UserIP = $_SERVER['REMOTE_ADDR'];
          }
        }
      }
      else{
        $UserIP = $_SERVER['REMOTE_ADDR'];
      }
      return $UserIP;
    }

The Problem is I've been getting problems with users operating via a proxy. Can anyone indicate why that might be? I've used basic free proxy's online to try and emulate, but it doesn't look to be getting variable IPs or anything - so I'm not sure why this would be saying the two IPs don't match.

waxical
  • 3,616
  • 7
  • 40
  • 66
  • what happens in your code if `$_SERVER['HTTP_X_FORWARDED_FOR']` has no `,` in it? and i don't get this part `if ($UserIP == $_SERVER['HTTP_X_FORWARDED_FOR']) { $UserIP = $_SERVER['REMOTE_ADDR']; } ` – Jan Prieser Oct 19 '12 at 09:31
  • Good question - in the case of no , - then it'd just leave it as HTTP_X_FORWARDED_FOR. The second question, it's saying if there's not an forwarded IP (that you get on a load balancer) - then take the REMOTE_ADDR - that's the most basic ip. – waxical Oct 19 '12 at 16:03

5 Answers5

2

I am going to explain what a proxy is first so we are both on the same page.

What Is A Proxy

A proxy is usually a single computer that accesses the internet ON BEHALF OF the user and then the proxy sends the results back to the user. The problem appears when there could be hundreds or even thousands of other people also using that one computer - and they all have the SAME IP address but normally the headers indicate that the users are via a proxy.

Your script i am assuming (without properly looking) is getting the IP's and headers mixed up.

Different Solution

There is a better way. Use sessions and save a key in the session ensuring they have previously been to the main site first BEFORE accessing the ajax page. Example:

index.php

session_start();
$_SESSION['ajax_ok'] = true;

ajax/username_check.php

session_start();
if (empty($_SESSION['ajax_ok'])) {
    die("You can not access this page...");
}

That will enforce that they must visit your main site first and the client must also support sessions which most browsers will which is a plus to help prevent bots etc abusing your ajax scripts.

More reliable and easier solution than using that mangle of code you got above ><

What if you can't use sessions?

If you can't use sessions on the specific comp you're on, you could setup another computer, rewrite the session handler (using the callbacks that php provides) or instead of using the file system use something else like the database for sessions instead? There must be something your website uses that the sessions could also use. Such as if you have a load balancer file based sessions generally wont work anyway so it's generally a good idea to change the sessions to use something else like i mentioned above.

HenchHacker
  • 1,606
  • 1
  • 10
  • 16
  • I think it's debatable as to whether this is a better way. It's another way, sure. There are situations when I can't use sessions so that's why a broader non-sessions approach was taken. Thanks for your suggestion tho and if no further, I might have to go down this line. I think someone downvoted you because it didn't actually answer the question, but I've upvoted back. – waxical Oct 26 '12 at 09:08
  • Thanks, you're probably right as to why they downvoted me, but i think one of the reasons no one has given you a straight answer is bccause it's extremely hard to answer without having a configuration setup that can be debugged. Hence why i gave you a little workaround. – HenchHacker Oct 26 '12 at 10:42
  • 1
    And if you can't use sessions on the specific comp you're on, you could setup another computer, rewrite the session handler (using the callbacks that php provides) or instead of using the file system use something else like the database for sessions instead? Such as if you have a load balancer file based sessions generally wont work anyway. – HenchHacker Oct 26 '12 at 10:44
2

The problem unfortunately almost certainly isn't a proxy - it's almost certainly a stationary public IP router, routing traffic through a subnet.

And subnets can be HUGE (say, at universities).

And even if it was by some fluke a genuine proxy (they are rare these days - 10 year old tech), even if the proxy volunteers to forward, it won't happen to mean anything because it's almost certainly a subnet ip like 192.168.x.x anyway.. This is basically the public IP address (aka switchboard) internal extension.

You could cross your fingers and try php ipv6 Working with IPv6 Addresses in PHP or even be even more clever and try mac address How can I get the MAC and the IP address of a connected client in PHP? but both are doomed to failure. My gut instinct is to try to cheat: I would gamble that the best way is basically using a network share for the session store and allowing the load balanced PHP servers to all access it and do everything via the same dns prefix. Or perhaps set up a 3rd party dns for doing the sessional gathering.

The answer is that unless you track with "cookies" like your an ad agency you can't do it.

Community
  • 1
  • 1
conners
  • 1,400
  • 4
  • 16
  • 27
  • The cookie seem to be the only real solution, if you really need to be "sure" who you are talking with. It should be easier to manage than ip. And unless someone stole the cookie, you know it is the same device than before. Anyway security based on ip or cookies, is still very light. I don't know what you are tying to achieve, but conners is right, cookies seem the best solution. – Perello Oct 31 '12 at 13:37
1

A friend identified this, basically some proxy's can come back with X_FORWARDED_FOR as a comma separated value OR as comma separated and spaced.

To fix:-

after:

foreach ($UserIPArray as $IPtoCheck) {

add this line:

$IPtoCheck = trim($IPtoCheck);

Sorted.

waxical
  • 3,616
  • 7
  • 40
  • 66
0

The Problem is I've been getting problems with users operating via a proxy. Can anyone indicate why that might be? I've used basic free proxy's online to try and emulate, but it doesn't look to be getting variable IPs or anything - so I'm not sure why this would be saying the two IPs don't match.

Your parsing code explodes HTTP_X_FORWARDED_FOR on comma, but separator may be "comma space". If that happens, the RFC 1918 check will fail. While some proxies do not add space, the standard is to use it:

http://en.wikipedia.org/wiki/X-Forwarded-For

The general format of the field is:

X-Forwarded-For: client, proxy1, proxy2

where the value is a comma+space separated list of IP addresses, the left-most being the original client, and each successive proxy that passed the request adding the IP address where it received the request from. In this example, the request passed proxy1, proxy2 and proxy3 (proxy3 appears as remote address of the request).

So you ought to change the explode separator to ", ", or better, use preg_split with ",\s*" as a separator and cover both cases.

Then, your problem is to authenticate the page doing the call in AJAX to the AJAX call itself.

If you don't want to use cookie-based sessions, which is the best way, you can try and do this with a nonce. That is, when you generate the page, you issue a unique ID and inject it into the HTML code, where the AJAX code will recover it and pass back to the AJAX servlet. The latter will be able to add it in Access-Control-Request, as detailed here, or just add more data to the request.

Community
  • 1
  • 1
LSerni
  • 49,775
  • 9
  • 56
  • 97
  • explode with comma is ok, but a `trim()` should be applied afterwards. `array_walk()` might be able to do this. – Sven Oct 31 '12 at 20:37
  • Yes, the best way IMHO is still using `preg_split`, which works either way and does not require `trim`. Actually, I wouldn't rely on headers at all; session is still the cleanest way. But if it can't be used, then `preg_split` provides a clean splitting interface. – LSerni Nov 01 '12 at 09:43
0

I'd like to discuss whether your solution does anything on behalf of security.

$_SERVER['REMOTE_ADDR'] cannot be forged. It is set by the webserver because of the accessing IP address used. Any response goes to this address.

$_SERVER['HTTP_FORWARDED_FOR'] and $_SERVER['HTTP_CLIENT_IP'] can easily be forged, because they are HTTP headers sent to the webserver - neither will you know you are talking to a proxy if it is configured to omit these headers, nor will you know you are NOT talking to a proxy if a client decides to insert these headers.

Filtering based on the IP address FORGED will not really help you, but this highly depends on the stuff you want to achieve - which remains unknown until you go into more detail there.

If you look around, you will stumble upon the Suhosin patch and extension for PHP, and it's feature to encrypt the session and cookie content. The encryption key is built by using some static key, but adding stuff like the HTTP User Agent or parts of the requesting IP address - note: The IP used to actually make the request, i.e. the proxy if one is used, because this is the only reliable info.

One can argue that using the full IP address is not a very good idea unless you know your users do not use proxy clusters with changing IP addresses for multiple requests, so only a part of the IP would usually be used, or none at all.

The HTTP User Agent though is a nice source of unique information. Yes, it can be forged, but that does not matter at all, because if you only want to allow requests from the same source, it is valid to assume that the user agent string does not change over time, but will be different for a bunch of other users. Hence there are statistics that show you can generate a nearly unique fingerprint of a browser if you just look at all the HTTP headers sent, because every user installs different extensions that change something, like accept-content headers.

I cannot provide working code, nor can I tell from your question whether any of this answer applies, but I'd suggest not using the IP info, especially if it can be forged. This is even more valid if you think about IPv6, where every client has multiple active addresses even on the same interface, and they are all randomly generated and highly unlikely to ever occur again later in time. (Of course this does not apply if you are never gonna host on IPv6, but at some point you'll be out of users then.)

Sven
  • 62,889
  • 9
  • 98
  • 100