Explica patrones de implementación defensivos para proteger su API de abusos.
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.
| Algoritmo | Características | Pros | Contras |
|---|---|---|---|
| Fixed Window | Recuentos dentro de una ventana temporal fija | Fácil de implementar, eficiente en memoria | Las explosiones se producen en los límites de las ventanas |
| Sliding Window Log | Fechas de solicitud de registros | Control preciso | Alto consumo de memoria |
| Sliding Window Counter | Media ponderada de las ventanas anterior y actual | Buen equilibrio | Algo complejo |
| Token Bucket | Consume fichas de acceso | Permite ráfagas al tiempo que impone límites | Requiere ajuste de parámetros |
| Leaky Bucket | Tramita las solicitudes a un ritmo constante | Velocidad de salida estable | Mal manejo de las ráfagas |
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);
RateLimit-Limit: 100 RateLimit-Remaining: 42 RateLimit-Reset: 1672531200 Retry-After: 30
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.
Todas las entradas de la API pueden contener datos maliciosos. Validar siempre en el lado del servidor.
Define una lista de valores permitidos. Más seguro que una lista de denegación (lista de bloqueo). Puede manejar nuevos patrones de ataque.
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.
Rechazar una entrada no válida es más seguro que "arreglarla". La desinfección puede producir transformaciones inesperadas.
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);
| Ataque | Contramedida | Ejemplo |
|---|---|---|
| Inyección SQL | Consultas parametrizadas, uso de un ORM | db.query('SELECT * FROM users WHERE id = ?', [id]) |
| Inyección NoSQL | Comprobació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 salida | Content-Type: application/json |
| Travesía de la ruta | Eliminar los separadores de ruta de la entrada | Normalizar con ruta.basename() |
| XXE (entidad externa XML) | Desactivar la resolución de entidades externas | Desactivar en la configuración del analizador XML |
CORS es un mecanismo que controla la Política de Mismo Origen del navegador. Una mala configuración puede conllevar graves riesgos de seguridad.
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.
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 }));
| Cabecera | Propósito | Valor recomendado |
|---|---|---|
Access-Control-Allow-Origin | Orígenes permitidos | Especificación explícita del dominio |
Access-Control-Allow-Methods | Métodos HTTP permitidos | Sólo se requiere un mínimo |
Access-Control-Allow-Headers | Cabeceras de solicitud permitidas | Sólo se requiere un mínimo |
Access-Control-Allow-Credentials | Permitir la transmisión de cookies | true (sólo cuando se requiere autenticación) |
Access-Control-Max-Age | Duración de la caché de verificación previa en segundos | 86400 (24 horas) |
Access-Control-Expose-Headers | Cabeceras de respuesta legibles por JavaScript | Sólo las cabeceras necesarias como RateLimit-* |
LLM APIs introduce new dimensions to rate limiting: token consumption, cost per request, and compute-intensive inference.
| Dimension | Traditional API | LLM API |
|---|---|---|
| Cost per request | Low, predictable | Variable, can be 100x+ (based on tokens) |
| Rate limit unit | Requests per time window | Tokens per minute (TPM) + Requests per minute (RPM) |
| Abuse pattern | Scraping, brute force, DDoS | Prompt injection, resource exhaustion, denial-of-wallet |
| Algorithm fit | Fixed/Sliding window | Token bucket (weighted by token count) |
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}
Related: LLM10: Model Denial of Service, ASI04: Cascading Hallucination Attacks
Prompt injection is the #1 risk for LLM applications. Apply defense-in-depth with input validation, structural separation, and output verification.
Strip or escape special tokens, instruction-like patterns, and control characters from user input before including in prompts.
Use delimiters, XML tags, or separate message roles to clearly isolate system instructions from user-provided content.
Validate LLM responses against expected schemas. Check for data leakage, instruction following, and malicious content before rendering.
| Ataque | Contramedida | Ejemplo |
|---|---|---|
| Direct Prompt Injection | Input sanitization + instruction/data separation | "Ignore previous instructions and..." |
| Indirect Prompt Injection | Sanitize RAG results + canary tokens | Malicious instructions hidden in retrieved documents |
| Context Stuffing | Token limits + input truncation | Overloading context window to push out system instructions |
| Parameter Tampering | Schema validation + typed parameters | Manipulating temperature, max_tokens, or model parameters |
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)
Related: LLM01: Prompt Injection, LLM02: Insecure Output Handling, ASI02: Prompt Injection via Tool Results
# 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=()