La mayoría de guías sobre seguridad web te dicen que instales mod_security en Apache o uses un WAF cloud como Cloudflare. Pero mod_security es complejo de configurar, propenso a falsos positivos, y requiere acceso root al servidor. Cloudflare es una empresa americana que intercepta todo tu tráfico TLS (sí, descifran y re-cifran tu HTTPS). ¿Y si pudieras tener un WAF directamente en PHP, sin módulos, sin dependencias, sin intermediarios?
Un WAF a nivel de aplicación tiene ventajas que uno a nivel de servidor no tiene: conoce el contexto de tu aplicación, puede tomar decisiones basadas en la sesión del usuario, y se despliega con un simple require_once. En este artículo vamos a construir los componentes esenciales de un WAF en PHP puro.
Arquitectura de un WAF PHP
Un WAF funciona como un filtro que intercepta cada petición HTTP antes de que llegue a tu lógica de negocio. En PHP, esto se implementa con un archivo que se incluye al principio de cada página (o vía auto_prepend_file en php.ini). El flujo es:
- 1. Recibir la petición HTTP (método, URI, headers, body)
- 2. Analizar cada parámetro contra patrones de ataque conocidos
- 3. Verificar rate limiting (peticiones por IP por ventana de tiempo)
- 4. Comprobar listas negras (IPs, User-Agents, países)
- 5. Si se detecta ataque: bloquear, registrar y responder con 403
- 6. Si es legítima: dejar pasar sin modificar nada
Detección de SQL Injection
La inyección SQL sigue siendo el ataque web más prevalente según OWASP Top 10. Un WAF debe detectar patrones de SQLi en todos los vectores de entrada: GET, POST, cookies y headers. La clave está en buscar patrones que un usuario legítimo nunca enviaría:
<?php
/**
* Detectar patrones de SQL Injection en un valor
* @param string $valor — Valor a analizar
* @return bool — true si se detecta SQLi
*/
function detectar_sqli($valor) {
$patrones = [
// Uniones y subconsultas
'/\bUNION\s+(ALL\s+)?SELECT\b/i',
'/\bSELECT\s+.*\bFROM\b/i',
// Manipulación de lógica
'/\bOR\s+[\'"]?\d+[\'"]?\s*=\s*[\'"]?\d+/i',
'/\bAND\s+[\'"]?\d+[\'"]?\s*=\s*[\'"]?\d+/i',
// Comentarios SQL (bypass de filtros)
'/\/\*.*?\*\//s',
'/--\s/',
// Funciones peligrosas
'/\b(SLEEP|BENCHMARK|LOAD_FILE|INTO\s+OUTFILE)\b/i',
// Stacked queries
'/;\s*(DROP|ALTER|CREATE|INSERT|UPDATE|DELETE)\b/i',
// Hex encoding bypass
'/0x[0-9a-f]{8,}/i',
];
foreach ($patrones as $patron) {
if (preg_match($patron, $valor)) {
return true;
}
}
return false;
}Este enfoque basado en regex tiene limitaciones — un atacante sofisticado puede encontrar formas de evasión. Pero combinado con prepared statements en tu código (que son la defensa primaria), el WAF actúa como capa de defensa en profundidad. No sustituye al código seguro; lo complementa.
Detección de XSS (Cross-Site Scripting)
Los ataques XSS inyectan JavaScript malicioso en parámetros que luego se renderizan en la página. Un WAF debe detectar tanto XSS reflejado como almacenado, buscando patrones de inyección de scripts:
<?php
function detectar_xss($valor) {
// Decodificar entidades HTML y URL encoding
$decodificado = html_entity_decode(urldecode($valor), ENT_QUOTES, 'UTF-8');
$patrones = [
// Tags de script
'/<script\b[^>]*>/i',
'/<\/script>/i',
// Event handlers
'/\bon(load|error|click|mouseover|focus|blur|submit)\s*=/i',
// javascript: en atributos
'/javascript\s*:/i',
// data: URIs con MIME ejecutable
'/data\s*:.*text\/html/i',
// SVG con scripts
'/<svg[^>]*on\w+\s*=/i',
// iframes
'/<iframe\b/i',
// Expression CSS (IE legacy pero aún peligroso)
'/expression\s*\(/i',
];
foreach ($patrones as $patron) {
if (preg_match($patron, $decodificado)) {
return true;
}
}
return false;
}Path Traversal y LFI
Los ataques de path traversal (../../etc/passwd) y Local File Inclusion (LFI) intentan acceder a archivos fuera del directorio web. Son especialmente peligrosos en PHP porque muchos desarrolladores construyen rutas de archivo con parámetros del usuario:
<?php
function detectar_path_traversal($valor) {
$decodificado = urldecode($valor);
$patrones = [
// Traversal clásico y variantes
'/\.\.\//',
'/\.\.\\/',
'/%2e%2e[\/\\]/i',
// Archivos sensibles del sistema
'/\/etc\/(passwd|shadow|hosts)/i',
'/\/proc\/self/i',
// Wrappers PHP (LFI to RCE)
'/php:\/\/filter/i',
'/php:\/\/input/i',
'/expect:\/\//i',
// Archivos de configuración
'/(wp-config|config|credentials|htpasswd)\.php/i',
'/\.env(\.|$)/',
];
foreach ($patrones as $patron) {
if (preg_match($patron, $decodificado)) {
return true;
}
}
return false;
}Rate Limiting sin Redis ni memcached
El rate limiting es esencial para prevenir fuerza bruta, DDoS a nivel de aplicación y scraping. La mayoría de implementaciones usan Redis o memcached, pero en PHP puro podemos usar archivos temporales con bloqueo atómico:
<?php
/**
* Rate limiter basado en ficheros con ventana deslizante
* @param string $ip — IP del cliente
* @param int $max — Máximo de peticiones por ventana
* @param int $ventana — Ventana en segundos (60 = 1 minuto)
* @return bool — true si se excede el límite
*/
function rate_limit_excedido($ip, $max = 60, $ventana = 60) {
$dir = sys_get_temp_dir() . '/waf_rl/';
if (!is_dir($dir)) mkdir($dir, 0700, true);
$archivo = $dir . md5($ip) . '.json';
$ahora = time();
// Leer registros existentes con bloqueo
$fp = fopen($archivo, 'c+');
flock($fp, LOCK_EX);
$datos = json_decode(fread($fp, filesize($archivo) ?: 1) ?: '[]', true) ?: [];
// Limpiar entradas fuera de ventana
$datos = array_filter($datos, fn($t) => $t > $ahora - $ventana);
// Añadir petición actual
$datos[] = $ahora;
// Escribir
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, json_encode(array_values($datos)));
flock($fp, LOCK_UN);
fclose($fp);
return count($datos) > $max;
}Integración: el filtro completo
Con los componentes individuales construidos, el WAF se integra como un único punto de entrada que analiza toda la petición:
<?php
// waf.php — incluir al principio de cada página
function waf_analizar_peticion() {
$ip = $_SERVER['REMOTE_ADDR'];
// 1. Rate limiting
if (rate_limit_excedido($ip, 100, 60)) {
http_response_code(429);
die('Demasiadas peticiones. Inténtalo en 1 minuto.');
}
// 2. Analizar todos los vectores de entrada
$vectores = array_merge(
$_GET, $_POST, $_COOKIE,
['uri' => $_SERVER['REQUEST_URI'] ?? ''],
['ua' => $_SERVER['HTTP_USER_AGENT'] ?? ''],
);
foreach ($vectores as $clave => $valor) {
if (!is_string($valor)) continue;
if (detectar_sqli($valor)) {
waf_bloquear($ip, 'SQLi', $clave, $valor);
}
if (detectar_xss($valor)) {
waf_bloquear($ip, 'XSS', $clave, $valor);
}
if (detectar_path_traversal($valor)) {
waf_bloquear($ip, 'LFI', $clave, $valor);
}
}
}
waf_analizar_peticion();Limitaciones y cuándo necesitas más
Un WAF PHP puro tiene limitaciones inherentes. Se ejecuta después de que PHP procese la petición, así que no puede proteger contra ataques a nivel de protocolo HTTP (slowloris, HTTP smuggling). Tampoco puede mitigar un DDoS volumétrico — para eso necesitas protección a nivel de red.
Pero para el 95% de ataques web que recibe una aplicación PHP — inyecciones SQL, XSS, path traversal, fuerza bruta, scraping — un WAF a nivel de aplicación es más que suficiente. Y tiene la ventaja crucial de entender el contexto de tu aplicación.
Escudo, el WAF de PWR.es, implementa todo lo descrito en este artículo más detección de bots, geo-blocking, listas negras dinámicas, integración con bases de datos de IPs maliciosas, y un panel de control en tiempo real. Todo en un solo archivo PHP de 40 KB, sin dependencias. Un require_once y tu web está protegida.
La seguridad web no requiere infraestructura compleja. Requiere código que entienda los ataques, que se ejecute rápido, y que no añada puntos de fallo. Un WAF en PHP nativo cumple las tres condiciones.