Limitación de velocidad

La limitación de velocidad es un mecanismo de defensa fundamental para proteger su sistema del abuso de la API, los ataques DoS y el scraping.

Comparación de algoritmos

AlgoritmoCaracterísticasProsContras
Fixed WindowRecuentos dentro de una ventana temporal fijaFácil de implementar, eficiente en memoriaLas explosiones se producen en los límites de las ventanas
Sliding Window LogFechas de solicitud de registrosControl precisoAlto consumo de memoria
Sliding Window CounterMedia ponderada de las ventanas anterior y actualBuen equilibrioAlgo complejo
Token BucketConsume fichas de accesoPermite ráfagas al tiempo que impone límitesRequiere ajuste de parámetros
Leaky BucketTramita las solicitudes a un ritmo constanteVelocidad de salida estableMal manejo de las ráfagas

Implementación de Rate Limiting en Express.js

JavaScript (Express)Limitación de velocidad multicapa
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');

// Límite global: se aplica a todas las API
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100,
  standardHeaders: true,    // Devuelve cabeceras RateLimit-*
  legacyHeaders: false,
  message: { error: 'Too many requests, please try again later.' },
});

// Para los puntos finales de autenticación: límites más estrictos
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,  // Intentos de inicio de sesión limitados a 5 cada 15 minutos
  skipSuccessfulRequests: true, // No se contabilizan las solicitudes realizadas con éxito
});

// Para entornos distribuidos: Redis backend
const distributedLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args) }),
  windowMs: 60 * 1000,
  max: 30,
});

app.use(globalLimiter);
app.use('/api/auth', authLimiter);

Encabezados de respuesta

HTTP Response HeadersRFC 6585 / draft-ietf-httpapi-ratelimit-headers
RateLimit-Limit: 100
RateLimit-Remaining: 42
RateLimit-Reset: 1672531200
Retry-After: 30
Teclas de limitación de velocidad

Al aplicar límites de velocidad utilizando claves compuestas como la clave API, el ID de usuario o el punto final, además de la dirección IP, puede minimizar el impacto en los usuarios legítimos.

🔍 Validación de entradas

Todas las entradas de la API pueden contener datos maliciosos. Validar siempre en el lado del servidor.

Principios de validación

Enfoque Allowlist

Define una lista de valores permitidos. Más seguro que una lista de denegación (lista de bloqueo). Puede manejar nuevos patrones de ataque.

Tipo, longitud y alcance

Compruebe estrictamente los tipos de datos, los recuentos máximos de caracteres y los límites numéricos superiores/inferiores. Una defensa fundamental contra los desbordamientos de búfer.

Sanear vs. Rechazar

Rechazar una entrada no válida es más seguro que "arreglarla". La desinfección puede producir transformaciones inesperadas.

Validación con esquema JSON

JavaScript (Express + Ajv)Validación de esquemas
const Ajv = require('ajv');
const addFormats = require('ajv-formats');

const ajv = new Ajv({ allErrors: true, removeAdditional: true });
addFormats(ajv);

// Esquema para la API de creación de usuarios
const createUserSchema = {
  type: 'object',
  required: ['name', 'email'],
  additionalProperties: false,
  properties: {
    name: {
      type: 'string',
      minLength: 1,
      maxLength: 100,
      pattern: '^[a-zA-Z0-9\\s\\-]+$', // Restringir a caracteres permitidos
    },
    email: {
      type: 'string',
      format: 'email',
      maxLength: 254,
    },
    age: {
      type: 'integer',
      minimum: 0,
      maximum: 150,
    },
  },
};

// Middleware de validación
function validateBody(schema) {
  const validate = ajv.compile(schema);
  return (req, res, next) => {
    if (!validate(req.body)) {
      return res.status(400).json({
        error: 'Validation failed',
        details: validate.errors,
      });
    }
    next();
  };
}

app.post('/api/users', validateBody(createUserSchema), createUser);

Ataques comunes y contramedidas de validación

AtaqueContramedidaEjemplo
Inyección SQLConsultas parametrizadas, uso de un ORMdb.query('SELECT * FROM users WHERE id = ?', [id])
Inyección NoSQLComprobación de tipos, limpieza de operadores $Asegurarse de que la entrada es una cadena (rechazar objetos)
XSS (a través de la respuesta de la API)Especifique Content-Type, escape de salidaContent-Type: application/json
Travesía de la rutaEliminar los separadores de ruta de la entradaNormalizar con ruta.basename()
XXE (entidad externa XML)Desactivar la resolución de entidades externasDesactivar en la configuración del analizador XML

🌐 CORS (Cross-Origin Resource Sharing)

CORS es un mecanismo que controla la Política de Mismo Origen del navegador. Una mala configuración puede conllevar graves riesgos de seguridad.

Configuración peligrosa

Access-Control-Allow-Origin: * y Access-Control-Allow-Credentials: true no pueden utilizarse juntos. Restrinja el uso de comodines únicamente a las API públicas.

Configuración CORS segura

JavaScript (Express)Configuración CORS
const cors = require('cors');

// Especificar explícitamente los orígenes permitidos
const allowedOrigins = [
  'https://app.example.com',
  'https://admin.example.com',
];

app.use(cors({
  origin(origin, callback) {
    // Permitir la comunicación de servidor a servidor (sin origen)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
  credentials: true,
  maxAge: 86400,  // Caché de verificación previa: 24 horas
}));

Lista de control CORS

Referencia de cabeceras CORS

CabeceraPropósitoValor recomendado
Access-Control-Allow-OriginOrígenes permitidosEspecificación explícita del dominio
Access-Control-Allow-MethodsMétodos HTTP permitidosSólo se requiere un mínimo
Access-Control-Allow-HeadersCabeceras de solicitud permitidasSólo se requiere un mínimo
Access-Control-Allow-CredentialsPermitir la transmisión de cookiestrue (sólo cuando se requiere autenticación)
Access-Control-Max-AgeDuración de la caché de verificación previa en segundos86400 (24 horas)
Access-Control-Expose-HeadersCabeceras de respuesta legibles por JavaScriptSólo las cabeceras necesarias como RateLimit-*

🤖 AI / LLM API Rate Limiting

LLM APIs introduce new dimensions to rate limiting: token consumption, cost per request, and compute-intensive inference.

Traditional API vs. LLM API Rate Limiting

DimensionTraditional APILLM API
Cost per requestLow, predictableVariable, can be 100x+ (based on tokens)
Rate limit unitRequests per time windowTokens per minute (TPM) + Requests per minute (RPM)
Abuse patternScraping, brute force, DDoSPrompt injection, resource exhaustion, denial-of-wallet
Algorithm fitFixed/Sliding windowToken bucket (weighted by token count)

Token-Aware Rate Limiting (Python)

PythonToken-Based Rate Limiter
import tiktoken
import time
from collections import defaultdict

class TokenRateLimiter:
    """Rate limiter that counts tokens, not just requests."""

    def __init__(self, tokens_per_minute=100_000, requests_per_minute=60):
        self.tpm_limit = tokens_per_minute
        self.rpm_limit = requests_per_minute
        self.usage = defaultdict(lambda: {"tokens": [], "requests": []})
        self.encoder = tiktoken.encoding_for_model("gpt-4")

    def count_tokens(self, text: str) -> int:
        return len(self.encoder.encode(text))

    def check_limit(self, user_id: str, prompt: str) -> dict:
        now = time.time()
        window = now - 60  # 1-minute sliding window
        user = self.usage[user_id]

        # Clean up expired entries
        user["tokens"] = [(t, c) for t, c in user["tokens"] if t > window]
        user["requests"] = [t for t in user["requests"] if t > window]

        # Check RPM
        if len(user["requests"]) >= self.rpm_limit:
            return {"allowed": False, "reason": "RPM limit exceeded"}

        # Check TPM
        token_count = self.count_tokens(prompt)
        used_tokens = sum(c for _, c in user["tokens"])
        if used_tokens + token_count > self.tpm_limit:
            return {"allowed": False, "reason": "TPM limit exceeded"}

        # Record usage
        user["tokens"].append((now, token_count))
        user["requests"].append(now)
        return {"allowed": True, "tokens_used": token_count}
OWASP References

Related: LLM10: Model Denial of Service, ASI04: Cascading Hallucination Attacks

🛡 Prompt Injection & AI Input Validation

Prompt injection is the #1 risk for LLM applications. Apply defense-in-depth with input validation, structural separation, and output verification.

Prompt Sanitization

Strip or escape special tokens, instruction-like patterns, and control characters from user input before including in prompts.

Structural Input Separation

Use delimiters, XML tags, or separate message roles to clearly isolate system instructions from user-provided content.

Output Verification

Validate LLM responses against expected schemas. Check for data leakage, instruction following, and malicious content before rendering.

Prompt Injection Attack Vectors & Mitigations

AtaqueContramedidaEjemplo
Direct Prompt InjectionInput sanitization + instruction/data separation"Ignore previous instructions and..."
Indirect Prompt InjectionSanitize RAG results + canary tokensMalicious instructions hidden in retrieved documents
Context StuffingToken limits + input truncationOverloading context window to push out system instructions
Parameter TamperingSchema validation + typed parametersManipulating temperature, max_tokens, or model parameters

Structured Input Validation (Python / Pydantic)

PythonPydantic Validation for LLM Requests
from pydantic import BaseModel, Field, field_validator
import re

class LLMRequest(BaseModel):
    """Validated LLM request with prompt injection defenses."""
    user_message: str = Field(..., max_length=4000)
    max_tokens: int = Field(default=1000, ge=1, le=4096)
    temperature: float = Field(default=0.7, ge=0.0, le=2.0)

    @field_validator("user_message")
    @classmethod
    def sanitize_prompt(cls, v: str) -> str:
        # Block known injection patterns
        patterns = [
            r"(?i)ignore\s+(previous|above|all)\s+(instructions?|prompts?)",
            r"(?i)you\s+are\s+now\s+",
            r"(?i)system\s*:\s*",
            r"(?i)\[INST\]|\[\/INST\]|<\|im_start\|>",
        ]
        for pattern in patterns:
            if re.search(pattern, v):
                raise ValueError("Input contains disallowed patterns")
        return v

# Usage with FastAPI
@app.post("/api/chat")
async def chat(request: LLMRequest):
    # Separate system instructions from user input
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": f"<user_input>{request.user_message}</user_input>"},
    ]
    return await call_llm(messages, request.max_tokens, request.temperature)
OWASP References

Related: LLM01: Prompt Injection, LLM02: Insecure Output Handling, ASI02: Prompt Injection via Tool Results

🔒 Otras cabeceras de seguridad recomendadas

HTTP Response HeadersAjustes recomendados
# Desactivar la detección automática del tipo de contenido
X-Content-Type-Options: nosniff

# Impedir la incrustación de iframe
X-Frame-Options: DENY

# Enforce HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains

# CSP: Como las API sólo devuelven JSON, bloquea toda ejecución de script
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'

# Restringir la información del remitente
Referrer-Policy: no-referrer

# Restringir funciones del navegador
Permissions-Policy: geolocation=(), camera=(), microphone=()