IP Address Reputation and intelligence plugin for WordPress

Shift8 IP Intelligence Reputation Wordpress Plugin

Hello!

With WordPress security, there are many methods for hardening and tightening controls, methods for preventing common attack vectors including best practices from a development, systems administration and even 3rd party plugin perspective.

Since the rising popularity of “IP Reputation Intelligence” with Corporate networks and streaming services like Netflix, I thought it would be a great opportunity to integrate one of the more powerful machine learning IP Intelligence services into WordPress : Shift8 IP Intel.

IP Intelligence is a free service among many paid / commercial alternatives. Though the free tier is limited by a request threshold, the opportunity to significantly reduce malicious traffic from visiting your site is obvious.

How this service works is it uses machine learning techniques developed by the service developer to identify your reputation and identify a scoring system associated with your IP. The higher the score, the more likely your IP address is part of a botnet, proxy, VPN, TOR and the like.

How we wanted to implement this plugin to use this service in WordPress is we wanted to provide a simple yet flexible interface to the IP Intelligence service. We also wanted to give the power to the WordPress admin to decide what actions to take based on the scoring threshold that would be associated with the visitor’s IP address.

Currently the only two options are to return a 403 (forbidden) error, or to force a 301 redirect away from the site. Further actions could provide even more flexibility such as creating a _SESSION variable that can in turn provide an avenue for custom actions defined in your theme’s functions.php (such as less visible actions such as removing the ability to pay for products on a site with credit card and limiting the user to only using bitcoin, for example).

I’ll break down some of the more interesting functions of this WordPress plugin for those who are interested.

Get your website visitors IP Address, regardless of your environment

This might seem like a simple objective, but with the advent of caching proxy services like Cloudflare, Cloudfront and Varnish, your visitor’s IP address isn’t stored or delivered by the standard way.

We needed a function to methodically obtain the end-user’s IP address through the many different standardized methods of storing the information :

function shift8_ipintel_get_ip() {
    // Methodically run through the environment variables that would store the public IP of the visitor
    $ip = getenv('HTTP_CLIENT_IP')?:
        getenv('HTTP_X_FORWARDED_FOR')?:
        getenv('HTTP_X_FORWARDED')?:
        getenv('HTTP_FORWARDED_FOR')?:
        getenv('HTTP_FORWARDED')?:
        getenv('REMOTE_ADDR');

    // Check if the IP is local, return false if it is
    if ( filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) ) {
        return $ip;
    } else {
        return false;
    }
}

What we’ve defined above is a segmented function that is solely responsible for methodically isolating the end-users’ real IP. The function is designed to accommodate many different environments, especially those that may be a bit more advanced such as those that are behind a load balancer or reverse proxy like Varnish or nginx.

When we do ultimately obtain the IP address with this function, we double check with the PHP function filter_var to validate that its a valid IP address and its not in the private IP address range.

Check the IP address against getipintel.net and store the results

Next we want to actually check the found IP address against getipintel.net. For future plugin updates, we might consider building an in-house IP reputation checking system that mimic’s (albeit in a rudimentary way) a simple reputation score from getipintel.net, however for now the plugin exclusively polls getipintel.net.

function shift8_ipintel_check($ip){
        $contact_email = esc_attr( get_option('shift8_ipintel_email'));
        $timeout = esc_attr(get_option('shift8_ipintel_timeout')); //by default, wait no longer than 5 secs for a response
        $ban_threshold = esc_attr(get_option('shift8_ipintel_actionthreshold')); //if getIPIntel returns a value higher than this, function returns true, set to 0.99 by default

        $response = wp_remote_get( "http://check.getipintel.net/check.php?ip=$ip&contact=$contact_email",
            array(
                'httpversion' => '1.1',
                'timeout' => $timeout,
            )
        );

        if ($response['body'] > $ban_threshold) {
                return 'banned';
        } else {
            if ($response['body'] < 0 || strcmp($response['body'], "") == 0 ) {
                // Return with encrypted error flag
                return 'error_detected';
            }
                return 'valid';
        }
}

The way we're polling would be similar to whats documented on the IP Intel website, though instead of Curl we are using wp_remote_get, which apparently falls back to Curl if the initial GET request fails.

You can see we are loading the contact email, timeout and ban threshold plugin options before making the request.

Validate the reputation score and store it in a SESSION variable

Once we've successfully made a check and obtained a reputation score (and based it on the plugin option defined threshold score that we want), we want to store the value in a php _SESSION variable.

if (!isset($_SESSION['shift8_ipintel']) || empty($_SESSION['shift8_ipintel'])) {
    // Only set the session if a valid IP address was found
    if ($ip_address) {
        $ip_intel = shift8_ipintel_check($ip_address);
        $session_data = $ip_address . '_' . $ip_intel . '_' . strtotime( '+1 day' );
        $session_value = shift8_ipintel_encrypt($encryption_key, $session_data);
        $_SESSION['shift8_ipintel'] = $session_value;
    }
}

If the session shift8_ipintel doesnt exist, then we first want to obtain the score and set it. This is actually where the shift8_ipintel_check function is first triggered. We dont want to trigger the function too many times as IP Intel has their own thresholds we need to honor.

You can see that we are setting the session data with the ip address, IP Intel score and then the date/time for 24 hours ahead in the future (when the session is supposed to expire or not be honored anymore). You can also see that we are actually encrypting the session data with a function called shift8_ipintel_encrypt. I wont go too much into the encrypt/decrypt functions as they aren't important for this, however if your interested you can read another blog post that was created that revolves more around encrypting and decrypting data in PHP.

What to do if the IP address reputation is poor

What we want to do is take the reputation score, if it exists as a session variable, and implement one of two actions (chosen by the end user) : 301 redirect or 403 forbidden.

                // if session is set, validate it and remove if not valid
                $session_data = explode('_', shift8_ipintel_decrypt($encryption_key, $_SESSION['shift8_ipintel']));
                // If the ip address doesnt match the encrypted value of the session
                if ($session_data[0] != $ip_address) {
                    clear_shift8_ipintel_session();
                } else if ($session_data[1] == 'banned') {
                    if (esc_attr(get_option('shift8_ipintel_action')) == '403') {
                        header('HTTP/1.0 403 Forbidden');
                        echo 'Forbidden';
                        die();
                    } else if (esc_attr(get_option('shift8_ipintel_action')) == '301') {
                        header("HTTP/1.1 301 Moved Permanently");
                        header("Location: " . esc_attr(get_option('shift8_ipintel_action301')));
                        die();
                    }
                } else if ($session_data[1] == 'error_detected') {
                    // Unset the existing session, re-set it with a shorter expiration time
                    clear_shift8_ipintel_session();
                    // Set the ip address but clear any IP Intel values for now
                    $session_newdata = $session_data[0] . '_ignore_' . strtotime( '+1 hour' );
                    $session_value = shift8_ipintel_encrypt($encryption_key, $session_newdata);
                    // Generally if there is an error detected, its likely because you exceeded the threshold. Wait an hour before doing this process again
                    $_SESSION['shift8_ipintel'] = $session_value;
                }

You can see around line 7 of the above snippet that we start to get into the logic of what to do if they fail the reputation threshold check. The two options (as of writing this) would be to return a 403 forbidden message or 301 redirect them (to whatever destination address was defined by the end user).

The last section of the snippet would be with the error handling logic. The most common scenario for an error would be someone exceeding the number of checks per hour that IP Intel (getipintel.net) defines. There is a paid tier where these thresholds are not applied. When using the plugin for the first time and initiating a request, getipintel.net should send an introductory email to whatever email you defined in the plugin settings. It is encouraged that you check that email if you are interested in purchasing a paid tier with a higher threshold.

What might be more useful beyond this, especially to other developers, would be a simple way that the session variable can be accessed or retrieved within WordPress that allows further logic and actions to take place based on the reputation score.

One possible scenario would be a Woocommerce (E-commerce) developer that wants to take action on a potential shopper that is about to buy something online but has a very poor IP reputation score. For example, perhaps they don't feel comfortable showing the Credit card form to a shopper with a very poor score because in theory that poor reputation (i.e. someone browsing the site via TOR) might have an increased risk of using a fraudulent credit card. Or perhaps they don't feel comfortable at all with someone of a poor reputation score and would like to remove any "add to cart" buttons for someone with said poor score.

The sky's the limit (as they say) and I plan on adding perhaps a function to use a shortcode or perhaps the option to not encrypt the session variable so it can be easily read and parsed outside of the plugin. For future plugin updates, we might add the ability to simply take no action but provide program-friendly ways to interact with the score elsewhere in WordPress.