Skip to content

Instantly share code, notes, and snippets.

@luckyshot
Last active April 28, 2023 07:46
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save luckyshot/6077693 to your computer and use it in GitHub Desktop.
Save luckyshot/6077693 to your computer and use it in GitHub Desktop.
Throttle client requests to avoid DoS attack (scroll down for IP-based)
<?php
/**
ABUSE CHECK
Throttle client requests to avoid DoS attack
*/
session_start();
$usage = array(5,5,5,5,10,20,30,40,50,60,120,180,240); // seconds to wait after each request
if (isset($_SESSION['use_last'])) {
$nextin = $_SESSION['use_last']+$usage[$_SESSION['use_count']];
if (time() < $nextin) {
echo 'Please wait '.($nextin-time()).' seconds&hellip;';
die();
}else{
$_SESSION['use_count']++;
if ($_SESSION['use_count'] > sizeof($usage)-1) {$_SESSION['use_count']=sizeof($usage)-1;}
}
}else{
$_SESSION['use_count'] = 0;
}
$_SESSION['use_last'] = time();
// Execute code here
<?php
/*
Limit amount of requests to avoid server abuse (IP based)
require( dirname(__FILE__) . '/lib/throttlerequest.php' );
$tr = new ThrottleRequest( $_SERVER['REMOTE_ADDR'], dirname(__FILE__) . '/lib/throttlerequest-db.php');
*/
class ThrottleRequest {
private $number_requests = 61;
private $time_interval = 60;
private $db_path = 'access.php';
private $db = [];
public function __construct( $ip, $path = NULL )
{
// if path is set
if ( isset( $path ) )
{
$this->db_path = $path;
}
// Get DB
if ( file_exists( $this->db_path ) )
{
$this->db = include( $this->db_path );
}
// Clean old IPs
foreach ( $this->db as $session_ip => $session )
{
if ( $session['expires'] <= time() )
{
unset( $this->db[ $session_ip ] );
}
}
// Add request
if ( isset( $this->db[ $ip ] ) )
{
$this->db[ $ip ]['requests']++;
}
else
{
$this->db[ $ip ] = [
'requests' => 1,
'expires' => time() + $this->time_interval,
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
];
}
// Save DB
file_put_contents( $this->db_path, '<?php return ' . var_export( $this->db, true ) . ';' );
// Check if needs to be blocked
if ( $this->db[ $ip ]['requests'] > $this->number_requests )
{
header('HTTP/1.0 503 Service Temporarily Unavailable');
header('Status: 503 Service Temporarily Unavailable');
header('Retry-After: ' . $this->time_interval ); // seconds
die( 'Too many requests. Please wait a few seconds.' );
}
}
}
<?php
/*
Limit amount of requests to avoid server abuse (IP based)
require( dirname(__FILE__) . '/libs/throttlerequest.php' );
$tr = new ThrottleRequest( $config );
MySQL
CREATE TABLE `throttlerequest` (
`ip` varchar(45) NOT NULL DEFAULT '',
`requests` int(11) NOT NULL DEFAULT '1',
`expires` datetime NOT NULL,
`country` char(2) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
`user_agent` varchar(255) DEFAULT NULL,
PRIMARY KEY (`ip`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
*/
class ThrottleRequest {
private $number_requests = 39;
private $time_interval = 20; // seconds
private $db = null;
private $config = null;
public function __construct( $config, $url = '' ){
$this->config = $config;
$this->db = new DB( $this->config['db_user'], $this->config['db_pass'], $this->config['db_dbname'] );
$ip = ( isset($_SERVER["HTTP_CF_CONNECTING_IP"]) ) ? $_SERVER["HTTP_CF_CONNECTING_IP"] : $_SERVER['REMOTE_ADDR'];
// Clean old IPs
$clean = $this->db->query("DELETE FROM throttlerequest WHERE expires <= :expires;")
->bind(':expires', date('Y-m-d H:i:s'))
->delete();
$exists = $this->db->query("SELECT ip, requests FROM throttlerequest WHERE ip = :ip LIMIT 1;")
->bind(':ip', $ip )
->single();
if ( $exists ){
$result = $this->db->query("UPDATE throttlerequest SET
requests = :requests,
url = :url,
user_agent = :user_agent
WHERE ip = :ip
LIMIT 1;")
->bind(':requests', $exists['requests'] + 1 )
->bind(':url', $url )
->bind(':user_agent', $_SERVER['HTTP_USER_AGENT'] )
->bind(':ip', $ip )
->update();
}else{
$result = $this->db->query("INSERT INTO `throttlerequest` (`ip`, `requests`, `expires`, `country`, `url`, `user_agent`) VALUES (
:ip,
:requests,
:expires,
:country,
:url,
:user_agent
);")
->bind(':ip', $ip )
->bind(':requests', 1 )
->bind(':expires', date('Y-m-d H:i:s', time() + $this->time_interval ) )
->bind(':country', @$_SERVER['HTTP_CF_IPCOUNTRY'] )
->bind(':url', $url )
->bind(':user_agent', $_SERVER['HTTP_USER_AGENT'] )
->update();
}
// Check if needs to be blocked
if ( $exists['requests'] > $this->number_requests ){
http_response_code( 429 );
header('Retry-After: ' . $this->time_interval );
die( 'Too many requests, please wait a few seconds' );
}
}
}
<?php
/**
* THROTTLE REQUESTS
*
* Limit amount of requests to avoid server abuse
* For increased security, rely on IP addresses instead of Sessions
*
* $number_requests to be made on each interval
* $time_interval seconds each interval lasts
*
* throttlerequest( 10, 60 ); // 1 request every 6 seconds
*/
function throttlerequest( $number_requests = 10, $time_interval = 60 )
{
if( !isset($_SESSION) ) { session_start(); }
// window not started or already finished
if ( !isset($_SESSION['window']) OR $_SESSION['window'] + $time_interval < time() )
{
$_SESSION['window'] = time();
$_SESSION['count'] = 1;
}
// $_SESSION['window'] started
else if ( $_SESSION['window'] + $time_interval >= time() )
{
$_SESSION['count']++;
if ( $_SESSION['count'] > $number_requests )
{
die( 'Too many requests. Please wait a few seconds.' );
}
}
}
throttlerequest( 10, 60 );
@Rotzbua
Copy link

Rotzbua commented Feb 16, 2021

  $ip = ( isset($_SERVER["HTTP_CF_CONNECTING_IP"]) ) ? $_SERVER["HTTP_CF_CONNECTING_IP"] : $_SERVER['REMOTE_ADDR'];

This is insecure if no cloudflare is used. PHP converts ever request header XYZ to HTTP_XYZ so an attacker can set any value for $ip.

@luckyshot
Copy link
Author

This is insecure if no cloudflare is used. PHP converts ever request header XYZ to HTTP_XYZ so an attacker can set any value for $ip.

Thank you @Rotzbua, do you know any solution to this? How do you get the real IP of the user or detect if Cloudflare is active? A list of Cloudflare IPs seems very unpractical...

@Rotzbua
Copy link

Rotzbua commented Feb 17, 2021

Not sure if it is the best way, but I would add a "secret" header to the request by cloudflares workers so the php script knows that it is a request over cloudflare.

@luckyshot
Copy link
Author

That's a great solution, I'd love to have something that doesn't need extra steps to set up but this is simple enough. Thank you mate!

@Dr-Nio
Copy link

Dr-Nio commented Nov 15, 2022

Thank you @Rotzbua for the valid observation but can one use the code below to validate IP addresses first, before using it in here

@Dr-Nio
Copy link

Dr-Nio commented Nov 15, 2022

Thank you @Rotzbua for the valid observation but can one use the code below to validate IP addresses first, before using it in here

/**

  • To validate if an IP address is both a valid and does not fall within
  • a private network range.
  • @param string $ip
    */
    function isValidIpAddress($ip)
    {
    if (filter_var($ip, FILTER_VALIDATE_IP,
    FILTER_FLAG_IPV4 |
    FILTER_FLAG_IPV6 |
    FILTER_FLAG_NO_PRIV_RANGE |
    FILTER_FLAG_NO_RES_RANGE) === false) {
    return false;
    }
    return true;
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment