Rate Limiting
Mecanismo de control que limita el número de peticiones que un cliente puede realizar a una API o servicio web en un período de tiempo determinado, previniendo abusos, ataques DDoS y API scraping.
¿Qué es el Rate Limiting?
El Rate Limiting (Limitación de Tasa) es un mecanismo de control de seguridad que restringe el número de peticiones que un cliente puede realizar a un servicio o API en un período de tiempo determinado. Es la primera línea de defensa contra ataques de API Scraping, denegación de servicio (DDoS), credential stuffing, y abuso de recursos computacionales.
La ausencia de límite de tasa es lo que convierte un endpoint legítimo en una fuente de datos masivos: no hace falta vulnerar nada, basta con preguntar muchas veces. Es un patrón recurrente en brechas de datos masivas. La falta de este control está detrás de varias filtraciones masivas por scraping: Facebook (533 millones de perfiles, 2021), Twitter (la autoridad irlandesa de protección de datos cifró en unos 5,4 millones los usuarios de los conjuntos publicados, 2022-2023) y la enumeración de contactos de WhatsApp, todas por consultas automatizadas a gran escala que un rate limiting efectivo habría frenado.
¿Por qué es crítico el Rate Limiting?
Escenario sin rate limiting:
## Un atacante puede hacer peticiones ilimitadas
import requests
api_url = "https://api.vulnerable-site.com/users/{user_id}"
## Extraer 1 millón de usuarios en pocas horas
for user_id in range(1, 1000001):
response = requests.get(api_url.format(user_id=user_id))
if response.status_code == 200:
save_to_database(response.json())
# Sin rate limiting, esto se ejecuta a máxima velocidadEscenario con rate limiting:
## Con rate limiting de 100 req/min, el script se bloquea
for user_id in range(1, 1000001):
response = requests.get(api_url.format(user_id=user_id))
if response.status_code == 429: # Too Many Requests
print(f"Rate limit alcanzado en user {user_id}")
retry_after = int(response.headers.get('Retry-After', 60))
time.sleep(retry_after)
# El atacante necesitaría 166 horas (7 días) en lugar de 2 horasPor qué importa el límite de tasa
La mayoría de las extracciones masivas de datos que acaban en un peritaje no explotan una vulnerabilidad de código: aprovechan que el endpoint responde tantas veces como se le pregunte. Cuando sí hay límite, el atacante ha de repartir las peticiones —proxies rotatorios, cuentas múltiples—, y eso deja un patrón mucho más visible en los registros, que es justo lo que un perito busca. Y cuando el atacante rota direcciones para diluir esa huella, la rotación misma deja su propio rastro.
Algoritmos de Rate Limiting
1. Token Bucket (Cubo de Tokens)
El algoritmo más popular por su flexibilidad para permitir ráfagas controladas.
Funcionamiento:
- Un “cubo” se llena con tokens a una tasa constante (ej. 100 tokens/minuto)
- Cada petición consume 1 token
- Si no hay tokens disponibles, la petición se rechaza (HTTP 429)
- El cubo tiene capacidad máxima (ej. 200 tokens) para permitir ráfagas breves
#!/usr/bin/env python3
"""
Implementación de Token Bucket para Rate Limiting
Autor: Jonathan Izquierdo - Perito Informático Forense
"""
import time
from threading import Lock
class TokenBucket:
"""
Algoritmo Token Bucket para rate limiting
Permite ráfagas de tráfico mientras mantiene una tasa promedio controlada
"""
def __init__(self, capacity, refill_rate):
"""
Args:
capacity: Número máximo de tokens (permite ráfagas)
refill_rate: Tokens añadidos por segundo
"""
self.capacity = capacity
self.tokens = capacity
self.refill_rate = refill_rate
self.last_refill = time.time()
self.lock = Lock()
def _refill(self):
"""Añade tokens basándose en el tiempo transcurrido"""
now = time.time()
elapsed = now - self.last_refill
# Calcular tokens a añadir
tokens_to_add = elapsed * self.refill_rate
# Actualizar tokens (sin exceder capacidad)
self.tokens = min(self.capacity, self.tokens + tokens_to_add)
self.last_refill = now
def consume(self, tokens=1):
"""
Intenta consumir tokens para una petición
Returns:
bool: True si la petición es permitida, False si se alcanzó el límite
"""
with self.lock:
self._refill()
if self.tokens >= tokens:
self.tokens -= tokens
return True
else:
return False
def get_wait_time(self, tokens=1):
"""
Calcula cuánto tiempo debe esperar el cliente
Returns:
float: Segundos a esperar antes de reintentar
"""
with self.lock:
self._refill()
if self.tokens >= tokens:
return 0
tokens_needed = tokens - self.tokens
wait_time = tokens_needed / self.refill_rate
return wait_time
## Ejemplo de uso en una API Flask
from flask import Flask, jsonify, request
import functools
app = Flask(__name__)
## Rate limiter: 100 req/min por IP, permitiendo ráfagas de hasta 200
rate_limiters = {}
def rate_limit(func):
"""Decorador para aplicar rate limiting a endpoints"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
client_ip = request.remote_addr
# Crear bucket para esta IP si no existe
if client_ip not in rate_limiters:
rate_limiters[client_ip] = TokenBucket(
capacity=200, # Permite ráfagas de 200 peticiones
refill_rate=100/60 # 100 tokens por minuto = 1.67/segundo
)
bucket = rate_limiters[client_ip]
if bucket.consume():
# Petición permitida
return func(*args, **kwargs)
else:
# Rate limit alcanzado
wait_time = bucket.get_wait_time()
return jsonify({
'error': 'Rate limit exceeded',
'retry_after': int(wait_time)
}), 429, {'Retry-After': str(int(wait_time))}
return wrapper
@app.route('/api/users/<int:user_id>')
@rate_limit
def get_user(user_id):
"""Endpoint protegido con rate limiting"""
# Lógica de la API
return jsonify({'user_id': user_id, 'name': 'John Doe'})Ventajas:
- Permite ráfagas de tráfico legítimas (ej. usuario cargando dashboard con múltiples peticiones)
- Tasa promedio controlada a largo plazo
- Fácil de implementar y entender
Desventajas:
- Posible race condition si no se usa lock (threading)
- Requiere mantener estado por cliente (memoria)
2. Leaky Bucket (Cubo Agujereado)
Procesa peticiones a tasa constante, independientemente de ráfagas.
Funcionamiento:
- Las peticiones se añaden a una cola (bucket)
- Se procesan a tasa fija (ej. 100 req/min)
- Si la cola se llena, se rechazan nuevas peticiones
import queue
import threading
import time
class LeakyBucket:
"""
Algoritmo Leaky Bucket para rate limiting
Procesa peticiones a tasa constante, suavizando ráfagas
"""
def __init__(self, capacity, leak_rate):
"""
Args:
capacity: Tamaño máximo de la cola
leak_rate: Peticiones procesadas por segundo
"""
self.capacity = capacity
self.leak_rate = leak_rate
self.queue = queue.Queue(maxsize=capacity)
# Thread que procesa peticiones a tasa constante
self.processor = threading.Thread(target=self._process_queue, daemon=True)
self.processor.start()
def _process_queue(self):
"""Procesa peticiones de la cola a tasa constante"""
while True:
if not self.queue.empty():
request = self.queue.get()
# Procesar petición
self._handle_request(request)
# Esperar según leak rate
time.sleep(1 / self.leak_rate)
def _handle_request(self, request):
"""Ejecuta la petición"""
# Lógica de procesamiento
pass
def add_request(self, request):
"""
Añade petición a la cola
Returns:
bool: True si se añadió, False si la cola está llena
"""
try:
self.queue.put(request, block=False)
return True
except queue.Full:
return FalseVentajas:
- Tasa de procesamiento perfectamente constante
- Protege backend de sobrecargas
- Suaviza ráfagas de tráfico
Desventajas:
- No permite ráfagas legítimas (ej. usuario cargando dashboard)
- Mayor latencia promedio
- Complejidad de implementación (requiere threading)
3. Fixed Window Counter (Contador de Ventana Fija)
El más simple: cuenta peticiones en ventanas de tiempo fijas.
import time
from collections import defaultdict
class FixedWindowCounter:
"""
Rate limiting con ventanas de tiempo fijas
Ejemplo: Máximo 100 peticiones por minuto
"""
def __init__(self, max_requests, window_seconds):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.counters = defaultdict(int)
self.windows = {}
def _get_current_window(self):
"""Calcula la ventana de tiempo actual"""
now = time.time()
return int(now // self.window_seconds)
def allow_request(self, client_id):
"""
Verifica si la petición está permitida
Args:
client_id: Identificador del cliente (IP, user_id, API key)
Returns:
bool: True si permitida, False si se alcanzó el límite
"""
current_window = self._get_current_window()
# Resetear contador si cambió la ventana
if self.windows.get(client_id) != current_window:
self.counters[client_id] = 0
self.windows[client_id] = current_window
# Verificar límite
if self.counters[client_id] < self.max_requests:
self.counters[client_id] += 1
return True
else:
return False
## Uso en API
rate_limiter = FixedWindowCounter(max_requests=100, window_seconds=60)
def api_endpoint(client_ip):
if rate_limiter.allow_request(client_ip):
return "Petición permitida"
else:
return "Rate limit alcanzado", 429Ventajas:
- Implementación trivial
- Muy eficiente (solo incrementa contador)
- Fácil de entender y debuggear
Desventajas:
- Vulnerabilidad de boundary: Un atacante puede hacer 100 peticiones al final de un minuto y otras 100 al inicio del siguiente (200 peticiones en 2 segundos)
4. Sliding Window Log (Ventana Deslizante con Log)
Soluciona el problema de boundary de Fixed Window.
import time
from collections import defaultdict, deque
class SlidingWindowLog:
"""
Rate limiting con ventana deslizante
Mantiene un log de timestamps de peticiones
Más preciso pero consume más memoria
"""
def __init__(self, max_requests, window_seconds):
self.max_requests = max_requests
self.window_seconds = window_seconds
# Deque para almacenar timestamps de peticiones por cliente
self.request_logs = defaultdict(deque)
def allow_request(self, client_id):
"""
Verifica si la petición está permitida
Returns:
bool: True si permitida
"""
now = time.time()
client_log = self.request_logs[client_id]
# Eliminar peticiones fuera de la ventana
cutoff_time = now - self.window_seconds
while client_log and client_log[0] < cutoff_time:
client_log.popleft()
# Verificar si se alcanzó el límite
if len(client_log) < self.max_requests:
client_log.append(now)
return True
else:
return False
def get_retry_after(self, client_id):
"""Calcula cuándo puede reintentar el cliente"""
if not self.request_logs[client_id]:
return 0
oldest_request = self.request_logs[client_id][0]
retry_time = oldest_request + self.window_seconds
return max(0, retry_time - time.time())Ventajas:
- Precisión absoluta (no hay vulnerabilidad de boundary)
- Justo para todos los clientes
Desventajas:
- Alto consumo de memoria (guarda timestamp de cada petición)
- Complejidad O(N) para limpiar logs antiguos
5. Sliding Window Counter (Ventana Deslizante Optimizada)
Combina lo mejor de Fixed Window y Sliding Window Log.
import time
from collections import defaultdict
class SlidingWindowCounter:
"""
Versión optimizada de Sliding Window
Usa dos ventanas (actual y previa) para aproximar ventana deslizante
Mucho más eficiente en memoria que Sliding Window Log
"""
def __init__(self, max_requests, window_seconds):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.current_window = defaultdict(int)
self.previous_window = defaultdict(int)
self.window_start = defaultdict(int)
def allow_request(self, client_id):
"""
Verifica petición usando algoritmo de ventana deslizante aproximada
Fórmula:
requests_in_sliding_window = current_window_count +
previous_window_count * overlap_percentage
"""
now = time.time()
# Determinar ventana actual
current_window_index = int(now // self.window_seconds)
# Si cambió la ventana, rotar
if self.window_start.get(client_id, 0) != current_window_index:
self.previous_window[client_id] = self.current_window[client_id]
self.current_window[client_id] = 0
self.window_start[client_id] = current_window_index
# Calcular porcentaje de solapamiento con ventana previa
elapsed_in_current = now % self.window_seconds
overlap_percentage = 1 - (elapsed_in_current / self.window_seconds)
# Aproximar número de peticiones en ventana deslizante
estimated_count = (
self.current_window[client_id] +
self.previous_window[client_id] * overlap_percentage
)
# Verificar límite
if estimated_count < self.max_requests:
self.current_window[client_id] += 1
return True
else:
return FalseVentajas:
- Muy eficiente (solo 2 contadores por cliente)
- Mitiga boundary attacks
- Buen equilibrio precisión/rendimiento
Desventajas:
- No es 100% preciso (es una aproximación)
- Ligeramente más complejo que Fixed Window
Recomendación Profesional
En mis auditorías de seguridad de APIs, recomiendo Sliding Window Counter para la mayoría de casos: ofrece protección robusta contra boundary attacks con overhead mínimo. Para APIs críticas (ej. autenticación), combino con Token Bucket para permitir ráfagas legítimas.
Implementación con Redis (Producción)
Para APIs en producción con múltiples servidores, el rate limiting debe ser distribuido.
import redis
import time
class DistributedRateLimiter:
"""
Rate Limiting distribuido usando Redis
Permite rate limiting consistente en clusters de servidores
"""
def __init__(self, redis_client, max_requests, window_seconds):
self.redis = redis_client
self.max_requests = max_requests
self.window_seconds = window_seconds
def allow_request_fixed_window(self, client_id):
"""
Fixed Window con Redis
Usa comandos atómicos de Redis para evitar race conditions
"""
current_window = int(time.time() // self.window_seconds)
key = f"rate_limit:{client_id}:{current_window}"
pipe = self.redis.pipeline()
pipe.incr(key)
pipe.expire(key, self.window_seconds * 2) # TTL para limpieza automática
results = pipe.execute()
request_count = results[0]
return request_count <= self.max_requests
def allow_request_sliding_window(self, client_id):
"""
Sliding Window Log con Redis Sorted Set
Usa timestamps como scores en un Sorted Set
"""
now = time.time()
key = f"rate_limit:sliding:{client_id}"
pipe = self.redis.pipeline()
# Eliminar peticiones fuera de la ventana
cutoff = now - self.window_seconds
pipe.zremrangebyscore(key, 0, cutoff)
# Contar peticiones en ventana
pipe.zcard(key)
# Añadir petición actual
pipe.zadd(key, {str(now): now})
# Establecer TTL para limpieza
pipe.expire(key, self.window_seconds * 2)
results = pipe.execute()
request_count = results[1] # Resultado de zcard
return request_count < self.max_requests
## Configuración en Django/Flask
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
rate_limiter = DistributedRateLimiter(
redis_client=redis_client,
max_requests=100,
window_seconds=60
)
## Middleware de Flask
@app.before_request
def check_rate_limit():
client_ip = request.remote_addr
if not rate_limiter.allow_request_sliding_window(client_ip):
return jsonify({'error': 'Rate limit exceeded'}), 429Para qué sirve un contador distribuido
El contador en Redis resuelve el problema que tiene el límite por instancia: con varios servidores detrás de un balanceador, cada uno cuenta por su cuenta y el límite efectivo se multiplica por el número de nodos. Tras la implementación, el volumen de peticiones automatizadas cayó drásticamente sin afectar al tráfico legítimo. No doy porcentaje: sería una cifra de un caso concreto presentada como resultado general.
Técnicas de Bypass y Evasión
Las técnicas con las que se evade un límite de tasa, y el rastro que deja cada una:
1. Rotating Proxies
## Atacante usa pool de proxies para rotar IPs
import requests
from itertools import cycle
## Pool de 1000 proxies residenciales
proxies_pool = [
{'http': 'http://proxy1.example.com:8080'},
{'http': 'http://proxy2.example.com:8080'},
# ... 998 proxies más
]
proxy_cycle = cycle(proxies_pool)
for user_id in range(1, 1000001):
proxy = next(proxy_cycle)
response = requests.get(
f'https://api.target.com/users/{user_id}',
proxies=proxy
)
# Con 1000 proxies y límite de 100 req/min/IP
# Capacidad real: 100,000 req/minDefensa forense: Detectar múltiples IPs con comportamiento idéntico:
- Mismo User-Agent
- Secuencia de endpoints accedidos
- Timing patterns similares
- IPs del mismo rango ASN (proveedores de proxies)
2. Distributed Scraping (Botnets)
Usar miles de dispositivos comprometidos para distribuir carga.
Detección forense:
-- Identificar IPs con comportamiento de botnet
SELECT
ip_address,
COUNT(*) as request_count,
user_agent,
COUNT(DISTINCT endpoint) as unique_endpoints,
MIN(timestamp) as first_seen,
MAX(timestamp) as last_seen
FROM api_logs
WHERE timestamp > DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY ip_address
HAVING request_count BETWEEN 10 AND 50 -- Botnets evitan destacar
AND unique_endpoints < 5 -- Comportamiento muy específico
ORDER BY request_count DESC;
-- Correlacionar IPs con mismo patrón
SELECT
a1.ip_address as ip1,
a2.ip_address as ip2,
COUNT(*) as matching_patterns
FROM api_logs a1
JOIN api_logs a2
ON a1.endpoint = a2.endpoint
AND a1.user_agent = a2.user_agent
AND ABS(TIMESTAMPDIFF(SECOND, a1.timestamp, a2.timestamp)) < 5
WHERE a1.ip_address != a2.ip_address
GROUP BY ip1, ip2
HAVING matching_patterns > 20
ORDER BY matching_patterns DESC;3. Timing Attacks (Boundary Exploitation)
Explotar reseteo de ventanas en Fixed Window Counter.
import time
import requests
def boundary_attack(api_url, max_requests=100):
"""
Explota boundary de Fixed Window Counter
Si el límite se resetea cada minuto exacto (00:00, 01:00, 02:00...),
el atacante puede:
- Hacer 100 peticiones a las 00:59
- Esperar 1 segundo
- Hacer otras 100 peticiones a las 01:00
Total: 200 peticiones en 2 segundos
"""
# Sincronizar con reseteo de ventana
now = time.time()
next_minute = (int(now) // 60 + 1) * 60
time.sleep(next_minute - now - 1) # Esperar hasta 1 segundo antes
# Fase 1: Hacer máximo de peticiones justo antes del reseteo
for i in range(max_requests):
requests.get(f"{api_url}/users/{i}")
# Fase 2: Esperar reseteo (1 segundo)
time.sleep(1)
# Fase 3: Hacer otras 100 peticiones (nueva ventana)
for i in range(max_requests, max_requests * 2):
requests.get(f"{api_url}/users/{i}")
# Resultado: 200 peticiones en ~2 segundosDefensa: Usar Sliding Window en lugar de Fixed Window.
4. Credential/Token Stuffing
Usar credenciales o API keys robadas de usuarios legítimos.
## Atacante con 500 API keys filtradas
stolen_api_keys = load_api_keys_from_breach()
## Distribuir carga entre todas las keys
for i, user_id in enumerate(range(1, 1000001)):
api_key = stolen_api_keys[i % len(stolen_api_keys)]
response = requests.get(
f'https://api.target.com/users/{user_id}',
headers={'Authorization': f'Bearer {api_key}'}
)
# Con 500 keys y límite de 100 req/min/key
# Capacidad: 50,000 req/minDefensa:
- Monitorizar uso anómalo de API keys
- Alertar si una key hace peticiones desde múltiples IPs simultáneamente
- Implementar fingerprinting de dispositivos (además de API key)
Análisis Forense de Bypass de Rate Limiting
Script de Detección Automatizada
#!/usr/bin/env python3
"""
Herramienta forense para detectar bypass de rate limiting
Autor: Jonathan Izquierdo - Perito Informático Forense
"""
import pandas as pd
import numpy as np
from collections import Counter
from datetime import datetime, timedelta
def detect_rate_limit_bypass(log_file):
"""
Analiza logs de API para detectar intentos de bypass de rate limiting
Returns:
dict: Hallazgos organizados por tipo de bypass
"""
# Parsear logs (asume formato CSV: timestamp,ip,endpoint,user_agent,status)
df = pd.read_csv(log_file, parse_dates=['timestamp'])
findings = {
'rotating_proxies': [],
'distributed_scraping': [],
'timing_attacks': [],
'stolen_credentials': []
}
# 1. Detectar rotating proxies
# Múltiples IPs con mismo User-Agent y patrón de comportamiento
grouped = df.groupby('user_agent').agg({
'ip': lambda x: list(x.unique()),
'endpoint': lambda x: list(x)
})
for user_agent, row in grouped.iterrows():
unique_ips = row['ip']
endpoints = row['endpoint']
if len(unique_ips) > 50: # Mismo UA desde >50 IPs diferentes
# Verificar si acceden a mismos endpoints
endpoint_patterns = Counter(endpoints)
if len(endpoint_patterns) < 10: # Comportamiento muy específico
findings['rotating_proxies'].append({
'user_agent': user_agent,
'ip_count': len(unique_ips),
'top_ips': unique_ips[:10],
'pattern': 'Mismo User-Agent desde múltiples IPs con patrón idéntico'
})
# 2. Detectar distributed scraping (botnet)
# IPs individuales con volumen moderado pero patrón idéntico
ip_groups = df.groupby('ip').agg({
'timestamp': 'count',
'endpoint': lambda x: tuple(sorted(set(x))),
'user_agent': 'first'
}).rename(columns={'timestamp': 'request_count'})
# Buscar grupos de IPs con mismo endpoint pattern
pattern_groups = ip_groups.groupby('endpoint').filter(lambda x: len(x) > 20)
for pattern, group in pattern_groups.groupby('endpoint'):
if 20 < group['request_count'].mean() < 100: # Volumen moderado (evita destacar)
findings['distributed_scraping'].append({
'pattern': pattern[:5], # Primeros 5 endpoints del patrón
'ip_count': len(group),
'avg_requests_per_ip': group['request_count'].mean(),
'total_requests': group['request_count'].sum()
})
# 3. Detectar timing attacks (boundary exploitation)
# Picos de tráfico justo después de minutos exactos
df['minute'] = df['timestamp'].dt.floor('min')
df['second'] = df['timestamp'].dt.second
# Contar peticiones en primeros 5 segundos de cada minuto
early_seconds = df[df['second'] <= 5].groupby(['ip', 'minute']).size()
# Contar peticiones en últimos 5 segundos del minuto anterior
late_seconds = df[df['second'] >= 55].groupby(['ip', 'minute']).size()
# IPs con patrones sospechosos de timing
for ip in df['ip'].unique():
ip_early = early_seconds.get(ip, pd.Series()).sum()
ip_late = late_seconds.get(ip, pd.Series()).sum()
if ip_early > 50 and ip_late > 50:
findings['timing_attacks'].append({
'ip': ip,
'requests_at_window_start': ip_early,
'requests_at_window_end': ip_late,
'pattern': 'Concentración de peticiones en boundaries de ventanas temporales'
})
# 4. Detectar uso de credenciales robadas
# Misma API key/token desde múltiples IPs geográficamente dispersas
if 'auth_token' in df.columns:
token_ips = df.groupby('auth_token')['ip'].apply(lambda x: list(x.unique()))
for token, ips in token_ips.items():
if len(ips) > 5: # Token usado desde >5 IPs
findings['stolen_credentials'].append({
'token': token[:20] + '...', # Ocultar token completo
'ip_count': len(ips),
'ips': ips[:10],
'pattern': 'Mismo token de autenticación desde múltiples IPs'
})
return findings
def generate_bypass_report(findings, output_file='bypass_report.txt'):
"""Genera informe forense de bypass de rate limiting"""
with open(output_file, 'w', encoding='utf-8') as f:
f.write("="*70 + "\n")
f.write("INFORME FORENSE: BYPASS DE RATE LIMITING\n")
f.write("="*70 + "\n\n")
f.write(f"Fecha: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"Perito: Jonathan Izquierdo - Perito Informático Forense\n\n")
total_incidents = sum(len(v) for v in findings.values())
f.write(f"TOTAL DE INCIDENTES DETECTADOS: {total_incidents}\n\n")
for category, incidents in findings.items():
if incidents:
f.write(f"\n{category.upper().replace('_', ' ')}:\n")
f.write(f"{'='*70}\n")
for i, incident in enumerate(incidents, 1):
f.write(f"\n{i}. {incident}\n")
f.write("\n" + "="*70 + "\n")
f.write("RECOMENDACIONES:\n")
f.write("- Implementar rate limiting multinivel (IP + User-Agent + API key)\n")
f.write("- Usar Sliding Window en lugar de Fixed Window\n")
f.write("- Monitorizar patrones de comportamiento con ML\n")
f.write("- Implementar CAPTCHA adaptativo para IPs sospechosas\n")
print(f"[+] Informe generado: {output_file}")
## Uso
if __name__ == "__main__":
findings = detect_rate_limit_bypass('/var/log/api/access.log')
generate_bypass_report(findings)Grandes brechas por ausencia de rate limiting
Dos brechas públicas conocidas ilustran el patrón. Las consultas SQL siguientes son reconstrucciones ilustrativas del tipo de análisis que se haría sobre los registros, no extractos de un caso concreto.
WhatsApp: enumeración masiva de contactos
Vulnerabilidad: API de búsqueda de contactos sin rate limiting.
Técnica de ataque:
- Enumeración secuencial de números telefónicos internacionales
- 200 IPs distribuidas (rotating proxies)
- Velocidad: 10-15 millones de consultas/hora
Reconstrucción del patrón de ataque:
-- Logs de API de WhatsApp (hipotético)
SELECT
ip_address,
COUNT(*) as queries,
MIN(phone_number) as first_number,
MAX(phone_number) as last_number,
MAX(phone_number) - MIN(phone_number) as range_size
FROM contact_search_logs
WHERE timestamp BETWEEN '2025-03-01' AND '2025-03-15'
GROUP BY ip_address
HAVING queries > 1000000
ORDER BY queries DESC;
-- Resultado: 200 IPs con ~17.5M queries cada una
-- Patrón: Enumeración secuencial perfecta (range_size ≈ queries)⚠️ Cuidado al citar la multa de 225 millones: existió —la impuso la autoridad irlandesa de protección de datos a WhatsApp—, pero aquel expediente examinó obligaciones de transparencia, no esta enumeración. Vincular las dos cosas es atribuir a una sanción real una causa que no tuvo.
Twitter: enumeración de correos en recuperación de contraseña
Vulnerabilidad: Endpoint de recuperación de contraseña sin rate limiting adecuado.
Técnica:
- Envío de emails de recuperación para verificar existencia de cuentas
- Si el email existe, Twitter devuelve confirmación
- Enumeración de bases de datos de emails filtrados
Qué se puede afirmar y qué no: el incidente está documentado, pero no existe una valoración pública auditable de lo que costó ni de lo que valía el conjunto en mercados ilícitos. Las cifras que circulan no tienen metodología detrás, y en un dictamen se sostienen solo con el daño acreditado por el propio afectado.
Marco Legal y Normativo
RGPD: dónde encaja el rate limiting, y dónde no
Art. 32 RGPD: Seguridad del tratamiento
“Teniendo en cuenta el estado de la técnica, los costes de aplicación, y la naturaleza, el alcance, el contexto y los fines del tratamiento, así como riesgos de probabilidad y gravedad variables para los derechos y libertades de las personas físicas, el responsable y el encargado del tratamiento aplicarán medidas técnicas y organizativas apropiadas para garantizar un nivel de seguridad adecuado al riesgo…”
Interpretación: el art. 32 no nombra el rate limiting. Obliga a aplicar medidas «apropiadas» al riesgo, teniendo en cuenta el estado de la técnica y los costes, y deja la elección concreta al responsable. En una API que expone datos personales y admite consulta masiva, limitar la tasa es difícil de descartar como medida apropiada — pero eso es un razonamiento que hay que hacer caso por caso, no un mandato que se pueda citar.
ISO/IEC 27001:2022
El anexo A de la norma no contiene un control que ordene «limitar accesos masivos». Lo que hay son controles de gestión de accesos y de monitorización, de los que el rate limiting puede ser una implementación entre otras. Citar un número de control para algo que no dice es lo que un contraperito busca.
Relación con Otros Conceptos
- API Scraping: Rate Limiting es la principal defensa contra API scraping
- IDOR: Combinación de IDOR + ausencia de rate limiting = desastre
- Insider Threats: Insiders pueden abusar de APIs internas sin rate limiting
Conclusión
El rate limiting es la primera línea de defensa contra los ataques automatizados sobre APIs, y su ausencia está detrás de brechas de cientos de millones de registros, con la consiguiente exposición a sanciones del RGPD y a daños reputacionales.
Recomendaciones finales:
- Nunca lances una API sin rate limiting: Es como dejar la puerta abierta
- Usa algoritmos robustos: Sliding Window Counter es el mejor equilibrio
- Implementa múltiples capas: Rate limiting por IP + User-Agent + API key + endpoint
- Monitoriza activamente: Alertas automáticas de patrones de bypass
- Combina con otros controles: CAPTCHA, WAF, autenticación robusta
Si necesitas auditar la seguridad de tu API, implementar rate limiting robusto, o análisis forense de un ataque de API scraping, puedo ayudarte a proteger tus datos y cumplir con el RGPD.
¿Sabrías qué demostrar si mañana te lo piden?
La diferencia entre un incidente gestionado y uno sancionado suele estar en qué quedó registrado, no en qué se hizo.
Referencias y fuentes
- Data Protection Commission (Irlanda) — DPC launches inquiry into Twitter concerning datasets: los conjuntos publicados afectaban a «approximately 5.4 million Twitter users worldwide», mapeando identificadores a correos y teléfonos.
- Data Protection Commission (Irlanda) — Decisión in WhatsApp inquiry: la sanción de 225 millones de euros examinó obligaciones de transparencia, no la enumeración de contactos.
- RGPD — Reglamento (UE) 2016/679, art. 32: exige medidas técnicas apropiadas al riesgo. No nombra el límite de tasa ni ninguna medida concreta: la elección es del responsable.
Última actualización: 29 de agosto de 2026 Categoría: Seguridad Código: SEC-012
Autor: Jonathan Izquierdo, Perito Informático Forense en Jaén Contacto: digitalperito.es
Preguntas Frecuentes
¿Qué es el Rate Limiting y para qué sirve?
El Rate Limiting es un control de seguridad que limita el número de peticiones que un cliente (IP, usuario, API key) puede hacer a un servicio en un periodo de tiempo. Previene ataques de denegación de servicio (DDoS), API scraping, credential stuffing, y abuso de recursos. Ejemplo: máximo 100 peticiones por minuto por IP.
¿Cuáles son los algoritmos de Rate Limiting más comunes?
Los principales son: Token Bucket (permite ráfagas controladas), Leaky Bucket (tasa constante), Fixed Window (contador por ventana temporal fija), Sliding Window Log (ventana deslizante precisa) y Sliding Window Counter (ventana deslizante optimizada). Cada uno tiene ventajas según el caso de uso.
¿Cómo se puede evadir el Rate Limiting?
Técnicas de bypass incluyen: rotating proxies (rotar IPs), distributed scraping (usar botnets), User-Agent randomization, cookies/tokens robados, timing attacks (sincronizar peticiones justo al reseteo del límite), y explotación de inconsistencias en ventanas de tiempo. La defensa requiere múltiples capas de rate limiting.
¿Cómo se detecta un bypass de Rate Limiting desde perspectiva forense?
Analizando logs para identificar: múltiples IPs con patrones de comportamiento idénticos (mismo User-Agent, secuencia de endpoints), volumen agregado anómalo desde rangos IP relacionados, timing patterns sospechosos (picos justo después de reset de ventana), y correlación de datos exfiltrados con múltiples sesiones aparentemente independientes.
Términos Relacionados
API Scraping
Técnica de extracción automatizada de datos mediante peticiones masivas a APIs web, explotando ausencia de rate limiting o controles de acceso para recopilar información a gran escala sin autorización.
Insider Threats
Amenazas a la seguridad de la información originadas por empleados, contratistas o colaboradores internos que acceden, exfiltran o sabotean datos de forma malintencionada o negligente, aprovechando su acceso legítimo.
Cadena de Custodia
Protocolo legal y técnico que documenta y sostiene la integridad y la autenticidad de la evidencia digital desde su hallazgo hasta su presentación en juicio. Un defecto en ella debilita la fuerza probatoria y abre la puerta a la impugnación, pero no produce por sí solo la inadmisión de la prueba.
¿Necesitas un peritaje forense?
Si necesitas ayuda profesional con análisis forense digital, estoy aquí para ayudarte.
Solicitar Consulta Gratuita
