Saltar al contenido principal

Clase 09 — Seguridad de APIs y Buenas Prácticas

OWASP API Security Top 10

#VulnerabilidadDescripciónMitigación
1BOLAAcceder a objetos de otros usuariosValidar ownership
2Broken AuthenticationAuth débil o ausenteJWT, OAuth2, MFA
3Broken Object Property Level AuthExponer campos sensiblesFiltrar respuestas
4Unrestricted Resource ConsumptionSin rate limitRate limiting
5Broken Function Level AuthAcceder a funciones de adminRBAC
6Unrestricted Access to Business FlowsAbuso de flujosValidar flujos
7Server Side Request ForgerySSRFValidar URLs
8Security MisconfigurationMala configuraciónHardening
9Improper Inventory ManagementAPIs sin documentarInventario
10Unsafe Consumption of APIsConfiar en APIs externasValidar 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íaPráctica
AuthUsar tokens con expiración (JWT), no API keys estáticas
HTTPSSiempre, sin excepción
Rate LimitImplementar y comunicar en headers
ValidaciónValidar TODA entrada (tipos, rangos, formato)
ErroresNo exponer stack traces ni info interna
CORSRestringir orígenes permitidos
LoggingRegistrar todas las peticiones (sin datos sensibles)
SecretosUsar gestores de secretos, nunca hardcodear
VersionadoVersionar desde el inicio (/v1/, /v2/)
DocsDocumentar con OpenAPI/Swagger
TestsSmoke, funcional, rendimiento, seguridad
MonitoreoMétricas, alertas, dashboards

Ejercicios

  1. Ejecutá el script de auditoría de seguridad contra 3 sitios web y compará resultados
  2. Implementá rate limiting en una API de FastAPI y probalo
  3. Creá un script de rotación de tokens que verifique el nuevo token antes de guardar
  4. Configurá Nginx como API Gateway con rate limiting y headers de seguridad
  5. Implementá un checklist de seguridad automatizado para tus APIs