Un pequeño script básico en PHP para WAF y Rate Limit
Código para invocar Redis, previamente tiene que estar instalado en el servidor apt install redis-server y la extensión para PHP apt install php-redis
<?php
// redis.php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
if (!$redis->ping()) {
die("No se pudo conectar a Redis");
}
?>
Código para el rate limit, el ‘endpoint’ lo podemos definir por diferentes tipos, por la URL, por IP por usuario autenticado, etc.., pudiendo además para cada ‘endpoint’ añadir un limite diferente.
<?php
// limit_rate.php
require_once 'redis.php';
function rate_limit_check(Redis $redis) {
$ip = $_SERVER['REMOTE_ADDR'];
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$endpoint = "{$method} {$path}";
$limit_strike = 10; // intentos en
$limit_time = 60; // segundos
$key = "rate_limit:{$endpoint}:{$ip}";
echo 'Limite de '.$limit_strike.' peticiones cada '.$limit_time.' segundos </br>';
$count = $redis->incr($key);
echo 'Intentos rate limit: <strong>'.$count.'</strong></br>';
if ($count === 1) {
$redis->expire($key, $limit_time);
}
if ($count > $limit_strike) {
http_response_code(429);
header('Retry-After: ' . $limit_time); // Se deberia omitir para que el 'atacante' no sepa el limite
exit("Has superado el límite de peticiones. Intenta de nuevo en $limit_time segundos.");
}
}
?>
Código para el WAF
<?php
// waf.php
require_once 'redis.php';
function waf_check(Redis $redis) {
$ip = $_SERVER['REMOTE_ADDR'];
$ban_key = "waf:ban:{$ip}";
$strike_key = "waf:strikes:{$ip}";
$limit_strike = 3;
if ($redis->exists($ban_key)) {
http_response_code(403);
exit("IP bloqueada temporalmente");
}
$patterns = [
// SQL Injection
'/select\s+.*\s+from/i',
'/union(\s+all)?\s+select/i',
'/insert\s+into/i',
'/update\s+.*\s+set/i',
'/delete\s+from/i',
'/drop\s+table/i',
'/--/',
'/#/', // SQL comment
'/\bOR\b\s+.*=/', // OR 1=1
'/\'\s*OR\s*1=1/i',
'/sleep\s*\(/i', // time-based SQLi
// XSS
'/<script\b[^>]*>(.*?)<\/script>/is',
'/onerror\s*=/i',
'/onload\s*=/i',
'/alert\s*\(/i',
'/<.*?javascript:.*?>/i',
'/<iframe.*?>/i',
'/<img\s+.*src=.*onerror=.*>/i',
'/document\.cookie/i',
'/document\.location/i',
// Command Injection
'/\b(exec|system|passthru|shell_exec|popen|proc_open)\b/i',
'/[|;&`]/', // shell chaining
// File inclusion / traversal
'/\.\.\/|\.\.\\\\/', // ../ or ..\ path traversal
'/etc\/passwd/i', // Unix file access
'/boot\.ini/i', // Windows file access
// Generic RCE/Abuse
'/base64_decode\(/i',
'/eval\(/i',
'/phpinfo\(/i',
];
$inputs = array_merge($_GET, $_POST, $_COOKIE);
$endpoint = $_SERVER['REQUEST_METHOD'] . ' ' . parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
foreach ($inputs as $value) {
if (is_array($value)) {
$value = json_encode($value);
}
foreach ($patterns as $pattern) {
if (preg_match($pattern, $value)) {
$strikes = $redis->incr($strike_key);
echo 'Intentos WAF:'.$strikes.'</br>';
if ($strikes === 1) {
$redis->expire($strike_key, 3600);
}
if ($strikes >= $limit_strike) {
$redis->set($ban_key, 1, 3600);
$redis->del($strike_key);
http_response_code(403);
exit("IP bloqueada por actividad sospechosa");
}
http_response_code(403);
exit("Actividad sospechosa detectada, intento $strikes de ".$limit_strike);
}
}
}
}
?>
Para desbloquear el rate limit, necesitas considerar cómo se definió el bloqueo inicial. La forma de desbloquearlo dependerá de si el endpoint se restringió por URL, por usuario autenticado o no autenticado, o por IP.
Este tipo de script tiene que estar muy bien securizado.
<?php
require_once 'redis.php';
$ip = $_GET['ip'] ?? null;
// $method = $_SERVER['REQUEST_METHOD'];
// $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// $endpoint = "{$method} {$path}";
if (!$ip) {
die("Especifica la IP con ?ip=xxx.xxx.xxx.xxx");
}
$ban_key = "waf:ban:$ip";
$strike_key = "waf:strikes:$ip";
// $key_rate_limit = "rate_limit:{$endpoint}:{$ip}";
$redis->del($ban_key);
$redis->del($strike_key);
// $redis->del($key_rate_limit);
echo "IP $ip desbloqueada correctamente.";
?>
Como usarlo, hay muchas formas, para este ejemplo he hecho lo siguiente, además añadiendo una lista blanca por IP, pero se podría añadir otros tipos de excepciones
<?php
require_once 'waf.php'; // Lógica del WAF
require_once 'ratelimit.php'; // Lógica de Rate Limiting
$lista_blanca = ['127.0.0.1', '192.168.1.1'];
if(!in_array($_SERVER['REMOTE_ADDR'], $lista_blanca)) {
// Ejecutar la verificación de WAF (bloqueo de ataques)
waf_check($redis);
// Ejecutar la verificación de Rate Limiting
rate_limit_check($redis);
}else{
echo 'Saltamos la protección';
}
?>
En esta lógica directamente le envía un exit al usuario, pero se podrían hacer otra cosas por ejemplo:
- Redirigir a una pagina mas amigable informado de que se ha detectado algo anormal
- Añadir retrasos aleatorios de entre 1-5 minutos si supera 20 intentos y si se pasa de los 40 intentos bloquear la IP durante X minutos/horas
Para que los bloqueos se puedan consultar habría que hacer un log, añadir cada intento o cuando lo bloquea a una base de datos de esta forma sabremos cuando nos atacan o si un usuario legitimo se ha bloqueado y poder desbloquearlo.