Clase 09 — Seguridad de APIs y Buenas Prácticas
OWASP API Security Top 10
| # | Vulnerabilidad | Descripción | Mitigación |
|---|---|---|---|
| 1 | BOLA | Acceder a objetos de otros usuarios | Validar ownership |
| 2 | Broken Authentication | Auth débil o ausente | JWT, OAuth2, MFA |
| 3 | Broken Object Property Level Auth | Exponer campos sensibles | Filtrar respuestas |
| 4 | Unrestricted Resource Consumption | Sin rate limit | Rate limiting |
| 5 | Broken Function Level Auth | Acceder a funciones de admin | RBAC |
| 6 | Unrestricted Access to Business Flows | Abuso de flujos | Validar flujos |
| 7 | Server Side Request Forgery | SSRF | Validar URLs |
| 8 | Security Misconfiguration | Mala configuración | Hardening |
| 9 | Improper Inventory Management | APIs sin documentar | Inventario |
| 10 | Unsafe Consumption of APIs | Confiar en APIs externas | Validar input |
HTTPS — Siempre
# ❌ NUNCA en producción
curl http://api.ejemplo.com/datos
# ✅ SIEMPRE HTTPS
curl https://api.ejemplo.com/datos
# Verificar certificado
curl -vI https://api.ejemplo.com 2>&1 | grep -E "SSL|TLS|certificate"
# Verificar expiración
echo | openssl s_client -servername api.ejemplo.com -connect api.ejemplo.com:443 2>/dev/null | \
openssl x509 -noout -dates
Autenticación segura
# ❌ MAL: Token en URL (queda en logs del servidor)
curl "https://api.com/datos?token=secreto123"
# ❌ MAL: Token hardcoded
curl -H "Authorization: Bearer abc123hardcoded" https://api.com
# ✅ BIEN: Token en header + variable de entorno
curl -H "Authorization: Bearer $API_TOKEN" https://api.com/datos
# ✅ BIEN: Token desde gestor de secretos
TOKEN=$(aws secretsmanager get-secret-value --secret-id api-key | jq -r '.SecretString')
curl -H "Authorization: Bearer $TOKEN" https://api.com
Rotación de tokens
#!/bin/bash
OLD_TOKEN="$API_TOKEN"
NEW_TOKEN=$(curl -s -X POST https://api.com/auth/tokens/rotate \
-H "Authorization: Bearer $OLD_TOKEN" | jq -r '.token')
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $NEW_TOKEN" https://api.com/health)
if [[ "$STATUS" == "200" ]]; then
echo "$NEW_TOKEN" > ~/.secrets/api-token
chmod 600 ~/.secrets/api-token
echo "✅ Token rotado exitosamente"
else
echo "❌ Nuevo token no funciona"
exit 1
fi
Rate Limiting — Implementar en tu API
# FastAPI con rate limiting
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from collections import defaultdict
import time
app = FastAPI()
rate_limits = defaultdict(list)
MAX_REQUESTS = 60
WINDOW = 60
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
client_ip = request.client.host
now = time.time()
rate_limits[client_ip] = [t for t in rate_limits[client_ip] if now - t < WINDOW]
if len(rate_limits[client_ip]) >= MAX_REQUESTS:
return JSONResponse(
status_code=429,
content={"error": "Rate limit exceeded"},
headers={"X-RateLimit-Limit": str(MAX_REQUESTS), "Retry-After": str(WINDOW)}
)
rate_limits[client_ip].append(now)
response = await call_next(request)
remaining = MAX_REQUESTS - len(rate_limits[client_ip])
response.headers["X-RateLimit-Limit"] = str(MAX_REQUESTS)
response.headers["X-RateLimit-Remaining"] = str(remaining)
return response
Headers de seguridad
# FastAPI: headers de seguridad en cada respuesta
from fastapi.middleware.cors import CORSMiddleware
# CORS restringido
app.add_middleware(
CORSMiddleware,
allow_origins=["https://mi-frontend.com"], # NO usar ["*"] en producción
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
@app.middleware("http")
async def security_headers(request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["Content-Security-Policy"] = "default-src 'none'"
return response
Verificar headers de seguridad
#!/bin/bash
# security-audit.sh
URL="${1:?Uso: $0 <url>}"
echo "🔒 Auditoría de Seguridad: $URL"
echo "════════════════════════════════════"
headers=$(curl -sI --max-time 10 "$URL")
check() {
local header="$1" required="${2:-recommended}"
if echo "$headers" | grep -qi "$header"; then
local value
value=$(echo "$headers" | grep -i "$header" | head -1 | cut -d: -f2- | xargs)
printf " ✅ %-35s %s\n" "$header" "$value"
else
if [[ "$required" == "required" ]]; then
printf " ❌ %-35s FALTANTE (requerido)\n" "$header"
else
printf " ⚠️ %-35s FALTANTE (recomendado)\n" "$header"
fi
fi
}
echo ""
echo "Headers de seguridad:"
check "Strict-Transport-Security" "required"
check "X-Content-Type-Options" "required"
check "X-Frame-Options" "required"
check "Content-Security-Policy" "recommended"
check "X-XSS-Protection" "recommended"
echo ""
echo "Headers de API:"
check "X-RateLimit-Limit"
check "X-Request-Id"
echo ""
server=$(echo "$headers" | grep -i "^server:" | head -1)
if [[ -z "$server" ]]; then
echo " ✅ Server header oculto"
else
echo " ⚠️ $server (considerar ocultar versión)"
fi
API Gateway pattern
┌───── ────────────┐
Clientes ────────► │ API Gateway │
│ • Rate limiting │
│ • Auth/AuthZ │
│ • Logging │
│ • CORS │
│ • SSL termination│
└───────┬─────────┘
┌───────────┼───────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Service A│ │ Service B│ │ Service C│
└──────────┘ └──────────┘ └──────────┘
# Nginx como API Gateway
upstream pokemon_api {
server pokemon-api:8000;
}
server {
listen 443 ssl;
server_name api.ejemplo.com;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Strict-Transport-Security "max-age=31536000" always;
location /api/v1/pokemon {
limit_req zone=api burst=20;
proxy_pass http://pokemon_api;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Request-ID $request_id;
}
location /internal/ {
deny all;
}
}
Secrets Management
# ✅ Usar gestor de secretos
# AWS Secrets Manager
aws secretsmanager get-secret-value --secret-id my-api/prod | jq -r '.SecretString'
# HashiCorp Vault
vault kv get -format=json secret/api-keys | jq -r '.data.data.API_TOKEN'
# Kubernetes Secrets
kubectl create secret generic api-keys --from-literal=api-token=mi-token-secreto
# GitHub Secrets (para CI/CD)
gh secret set API_TOKEN --body "mi-token-secreto"
# En workflow: ${{ secrets.API_TOKEN }}
Buenas prácticas resumen
| Categoría | Práctica |
|---|---|
| Auth | Usar tokens con expiración (JWT), no API keys estáticas |
| HTTPS | Siempre, sin excepción |
| Rate Limit | Implementar y comunicar en headers |
| Validación | Validar TODA entrada (tipos, rangos, formato) |
| Errores | No exponer stack traces ni info interna |
| CORS | Restringir orígenes permitidos |
| Logging | Registrar todas las peticiones (sin datos sensibles) |
| Secretos | Usar gestores de secretos, nunca hardcodear |
| Versionado | Versionar desde el inicio (/v1/, /v2/) |
| Docs | Documentar con OpenAPI/Swagger |
| Tests | Smoke, funcional, rendimiento, seguridad |
| Monitoreo | Métricas, alertas, dashboards |
Ejercicios
- Ejecutá el script de auditoría de seguridad contra 3 sitios web y compará resultados
- Implementá rate limiting en una API de FastAPI y probalo
- Creá un script de rotación de tokens que verifique el nuevo token antes de guardar
- Configurá Nginx como API Gateway con rate limiting y headers de seguridad
- Implementá un checklist de seguridad automatizado para tus APIs