Explica os padrões de implementação defensiva para proteger sua API contra abusos.
A limitação de taxa é um mecanismo de defesa fundamental para proteger seu sistema contra abuso de API, ataques de DoS e scraping.
| Algoritmo | Características | Prós | Contras |
|---|---|---|---|
| Fixed Window | Contagens em um intervalo de tempo fixo | Simples de implementar, eficiente em termos de memória | As explosões ocorrem nos limites das janelas |
| Sliding Window Log | Carimbos de data e hora da solicitação de registros | Controle preciso | Alto consumo de memória |
| Sliding Window Counter | Média ponderada das janelas anterior e atual | Bom equilíbrio | Um pouco complexo |
| Token Bucket | Consome tokens para acesso | Permite explosões enquanto impõe limites | Requer ajuste de parâmetros |
| Leaky Bucket | Processa solicitações em uma taxa constante | Taxa de saída estável | Fraco em lidar com explosões |
const rateLimit = require('express-rate-limit'); const RedisStore = require('rate-limit-redis'); // Limite global: aplicado a todas as APIs const globalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutos max: 100, standardHeaders: true, // Retorna cabeçalhos RateLimit-* legacyHeaders: false, message: { error: 'Too many requests, please try again later.' }, }); // Para pontos de extremidade de autenticação: limites mais rígidos const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, // Tentativas de login limitadas a 5 a cada 15 minutos skipSuccessfulRequests: true, // As solicitações bem-sucedidas não são contadas }); // Para ambientes distribuídos: backend do Redis 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
Ao aplicar limites de taxa usando chaves compostas, como chave de API, ID de usuário ou ponto de extremidade, além do endereço IP, é possível minimizar o impacto sobre os usuários legítimos.
Todas as entradas de API podem conter dados maliciosos. Sempre valide no lado do servidor.
Define uma lista de valores permitidos. Mais seguro do que uma denylist (lista de bloqueio). Pode lidar com novos padrões de ataque.
Verifique rigorosamente os tipos de dados, a contagem máxima de caracteres e os limites numéricos superior/inferior. Uma defesa fundamental contra estouros de buffer.
Rejeitar uma entrada inválida é mais seguro do que "consertá-la". A higienização pode produzir transformações inesperadas.
const Ajv = require('ajv'); const addFormats = require('ajv-formats'); const ajv = new Ajv({ allErrors: true, removeAdditional: true }); addFormats(ajv); // Esquema para a API de criação de usuários 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 validação 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 | Exemplo |
|---|---|---|
| Injeção de SQL | Consultas parametrizadas, use um ORM | db.query('SELECT * FROM users WHERE id = ?', [id]) |
| Injeção de NoSQL | Verificação de tipo, higienização de operadores $ | Garantir que a entrada seja uma cadeia de caracteres (rejeitar objetos) |
| XSS (via resposta da API) | Especificar Content-Type, escapar da saída | Content-Type: application/json |
| Transversão de caminho | Remover separadores de caminho da entrada | Normalize com path.basename() |
| XXE (Entidade Externa XML) | Desativar a resolução de entidades externas | Desativar nas configurações do analisador XML |
O CORS é um mecanismo que controla a política de mesma origem do navegador. A configuração incorreta pode levar a sérios riscos de segurança.
Access-Control-Allow-Origin: * e Access-Control-Allow-Credentials: true não podem ser usados juntos. Restringir o uso de curingas somente a APIs públicas.
const cors = require('cors'); // Especificar explicitamente as origens permitidas const allowedOrigins = [ 'https://app.example.com', 'https://admin.example.com', ]; app.use(cors({ origin(origin, callback) { // Permitir a comunicação de servidor para servidor (sem origem) 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, // Cache de pré-voo: 24 horas }));
| Cabeçalho | Finalidade | Valor recomendado |
|---|---|---|
Access-Control-Allow-Origin | Origens permitidas | Especificação explícita do domínio |
Access-Control-Allow-Methods | Métodos HTTP permitidos | Mínimo exigido apenas |
Access-Control-Allow-Headers | Cabeçalhos de solicitação permitidos | Mínimo exigido apenas |
Access-Control-Allow-Credentials | Permitir a transmissão de cookies | true (somente quando a autenticação é necessária) |
Access-Control-Max-Age | Duração do cache do preflight em segundos | 86400 (24 horas) |
Access-Control-Expose-Headers | Cabeçalhos de resposta legíveis por JavaScript | Somente os cabeçalhos necessários, 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 | Exemplo |
|---|---|---|
| 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
# Desativar a detecção automática de tipo de conteúdo X-Content-Type-Options: nosniff # Impedir a incorporação de iframe X-Frame-Options: DENY # Aplicar HTTPS Strict-Transport-Security: max-age=31536000; includeSubDomains # CSP: como as APIs retornam apenas JSON, bloqueie toda a execução de scripts Content-Security-Policy: default-src 'none'; frame-ancestors 'none' # Restringir informações do referenciador Referrer-Policy: no-referrer # Restringir os recursos do navegador Permissions-Policy: geolocation=(), camera=(), microphone=()