Seguridad

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.

8 min de lectura

¿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 velocidad

Escenario 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 horas
Por 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 False

Ventajas:

  • 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", 429

Ventajas:

  • 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 False

Ventajas:

  • 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'}), 429
Para 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/min

Defensa 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 segundos

Defensa: 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/min

Defensa:

  • 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.

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

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:

  1. Nunca lances una API sin rate limiting: Es como dejar la puerta abierta
  2. Usa algoritmos robustos: Sliding Window Counter es el mejor equilibrio
  3. Implementa múltiples capas: Rate limiting por IP + User-Agent + API key + endpoint
  4. Monitoriza activamente: Alertas automáticas de patrones de bypass
  5. 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

  1. 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.
  2. 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.
  3. RGPDReglamento (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.

¿Necesitas un peritaje forense?

Si necesitas ayuda profesional con análisis forense digital, estoy aquí para ayudarte.

Solicitar Consulta Gratuita
Jonathan Izquierdo

Jonathan Izquierdo · Perito Forense

+15 años experiencia · AWS Certified

WhatsApp