Simple PHP DoS/brute-force throttling

I previously posted a PHP function for throttling the number of login attempts a user is allowed to make in any specific amount of time. So for example, if a user trying to login gets the password wrong 5 times in a row, s/he is not allowed to make another attempt for another 5 minutes. This is a pretty common feature with many forums/blogs and other online systems.

The function can be easily generalized to protect any page against concurrent requests, not just logins. This can come in handy if you have a particular report, for example, that uses a lot of memory or processing power to run. A malicious user could keep hitting this page to bring your server down. Passing the user request through a simple check to throttle the requests/minute/page can protect against a Denial of Service attack or brute-force login crack.

Just download the following static class, include it in your scripts, and call the throttleResource() method as described in the inline comments:

<?php 
class SecurityUtil {
  /**
     * $resource - a string representing the page name or URL or any other key you want to use to throttle requests for.
   * $rate - an int specifying the maximum number of allowed access requests per window period
   * $window - number of seconds representing your throttle window
   * $any_user - if false, the $_SERVER['REMOTE_ADDR'] IP of the client making the request is used to throttle number of requests
   * only for that client. If set to true, then the resource is throttled globally against requests from ANY user IP (good for DDoS prevention).
   *
   * returns nothing if the user should not be throttled, throws an Exception if user has exceeded allowed number of requests.
   * 
   * NOTE: The constant THROTTLE_FILE needs to be defined, which should point to the full path on the server to a text file that is writable by Apache.
   * Usage: just call this function whenever you process a request that you want to throttle, passing in the request-url (or some other identifying key),
   * and the throttle parameters. For example:
   * try {
   *     SecurityUtil::throttleResource('/admin/login-page', 5, 120);  //this limits the number of times a user can access the login page to 5 every 2 minutes.
   *     //if code gets to here, no exception thrown, which means user has not exceeded allowed number of attempts, so process the login (or whatever request)
   * } catch (Exception $e) {
   *     echo "Number of allowed login attempts exceeded. Please try again later.";
   *     exit;
   * }
   */
    public static function throttleResource($resource, $rate = 7, $window = 60, $any_user = false) {
        //load the cache from file
        $cache = file_exists(THROTTLE_FILE) ? self::varFromFile(THROTTLE_FILE) : array();    //load php array from file
    $user = $any_user ? '0.0.0.0' : $_SERVER['REMOTE_ADDR'];
    
        //initialize cache variables
        if(!isset($cache[$user])) $cache[user] = array();
        if(!isset($cache[$user][$resource])) $cache[$user][$resource] = array();

        //timestamp the cache resource access
        $cache[$user][$resource][] = time();
        //only hold the required number of timestamps
        if(count($cache[$user][$resource]) > $rate) array_shift($cache[$user][$resource]);

        //check if resource has exceeded allowed access requests
        $deny_access = false;
        $attempts = $cache[$user][$resource];
        if(count($attempts) < $rate) $deny_access = false;      //didn't exceed allowed access requests
        elseif($attempts[0] + $window > time()) $deny_access = true;  //else matched or exceeded allowed requests, and oldest access was within the window
        else $deny_access = false;                    //otherwise oldest access was not within window, so allow

        //cleanup the cache so it doesn't get too large over time
        foreach($cache as $ip=>$resources) {                    //for each IP address record
            foreach($resources as $res=>$attempts) {            //for each IP resource bucket
                if($attempts) {                                 //if resource access record exist
                    if($attempts[count($attempts)-1] + $window < time()) {  //if latest record is older than window
                        unset($cache[$ip][$res]);                           //irrelevant, delete the record
                    }
                }
            }
            if(!$resources) unset($cache[$ip]);                 //if the IP record has no resource buckets, delete it
        }

        //store the cache back to file
        self::varToFile(THROTTLE_FILE, $cache);

        if($deny_access) throw new Exception('Maximum access requests exceeded.');
    }
  
  public static function varToFile($filename, $var) {
        $data = gzcompress(serialize($var),9);
        $res = self::stringToFile($filename, $data);
        return $res;
    }

    public static function varFromFile($filename) {
        if(file_exists($filename)) {
            $data = unserialize(gzuncompress(file_get_contents($filename)));
            if($data) return $data;
            else return false;
        } else return false;
    }
  
  public static function fileToString($filename) {
        return file_get_contents($filename);
    }

    public static function stringToFile($filename, $data) {
        $file = fopen ($filename, "w");
        fwrite($file, $data);
        fclose ($file);
        return true;
    }
}
?>

Comments

  1. Hi Richard! This script could save my day but I'm running into a few issues. Would you be available to help by any chance? It might take you just a glance. Cheers!

    ReplyDelete
  2. Doesn't work here :(, as soon as i include the code i get no output back anymore. (Even without the function, just the include)

    ReplyDelete
  3. please provide demo & demo download files

    ReplyDelete
  4. Is it safe to assume this code is in the public domain?
    Or is there another license you prefer?
    I'd like to use this in a project, with attribution of course, and might publish the project to github.

    ReplyDelete
  5. It's public domain, do what you like with it, no credit required.

    Note that it's old and was never really stress-tested in any way. There may be better methods to do this these days, e.g. a standard library of some sort.

    ReplyDelete

Post a Comment

Popular posts from this blog

Wkhtmltopdf font and sizing issues

Import Google Contacts to Nokia PC Suite

Can't delete last blank page from Word