Seguridad de APIs Python con OWASP

Una API Python segura no aparece al instalar un middleware. La respuesta rápida es: autentica a quien llama, autoriza cada acción y objeto, valida toda entrada, permite solo los orígenes necesarios, limita el abuso, separa los secretos del código y registra eventos sin guardar credenciales. Los controles deben cubrir diseño, aplicación, infraestructura y pruebas.

El proyecto OWASP API Security organiza riesgos recurrentes, pero no es una lista para revisar después del despliegue. Úsalo al modelar amenazas: qué datos existen, quién puede leerlos, cómo automatizaría solicitudes un atacante y qué permitiría una credencial robada. La REST Security Cheat Sheet de OWASP aporta recomendaciones operativas.

Autenticación no significa autorización

Autenticación (authn) establece la identidad. Autorización (authz) decide si esa identidad puede realizar la operación sobre ese recurso. Un token válido no concede automáticamente acceso a GET /invoices/42. El servidor debe comprobar que la factura pertenece a quien llama o que un rol permite consultarla. Este control por objeto evita una clase crítica de autorización rota.

Prefiere protocolos establecidos, tokens breves y permisos pequeños. Verifica firma, emisor, audiencia, vencimiento y un algoritmo configurado explícitamente en el servidor. No confíes en el algoritmo que propone un token no fiable. Para sesiones web, cookies Secure, HttpOnly y un SameSite adecuado reducen exposición. La documentación de seguridad de FastAPI describe componentes OAuth2; la política de acceso sigue perteneciendo a tu aplicación.

Mantén la regla cerca de la carga del recurso:

from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, status
from pydantic import BaseModel, ConfigDict

app = FastAPI()

class User(BaseModel):
    id: int
    is_admin: bool = False

class Invoice(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: int
    owner_id: int
    total_cents: int

def current_user() -> User:
    # En producción, valida aquí una credencial real y sus claims.
    return User(id=7)

def load_invoice(invoice_id: int) -> Invoice | None:
    return Invoice(id=invoice_id, owner_id=7, total_cents=5900)

@app.get("/invoices/{invoice_id}")
def get_invoice(invoice_id: int, user: Annotated[User, Depends(current_user)]):
    invoice = load_invoice(invoice_id)
    if invoice is None:
        raise HTTPException(status_code=404, detail="Invoice not found")
    if invoice.owner_id != user.id and not user.is_admin:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
    return invoice

No uses un owner_id recibido en el cuerpo como fuente de propiedad: derívalo de la identidad autenticada. La guía de FastAPI para APIs RESTful presenta las bases del framework.

Validar entradas y controlar respuestas

Valida tipo, longitud, formato, rango y significado de negocio. Rechazar campos desconocidos reduce la asignación masiva: un cliente no puede añadir is_admin=true a un modelo permisivo. Limita tamaño total del cuerpo, número de elementos, archivos y profundidad estructural en el proxy o servidor. Los serializers definen estos contratos, como explica la guía de Django REST Framework, pero SQL requiere parámetros y las rutas de archivos necesitan reglas específicas.

No devuelvas tracebacks, fragmentos SQL, rutas internas ni detalles criptográficos. Un error público puede contener una descripción estable y un request_id; el diagnóstico completo corresponde a telemetría interna protegida. La guía de manejo de errores en Python ayuda a evitar capturas generales que ocultan fallos importantes.

CORS y rate limiting resuelven problemas distintos

CORS indica a los navegadores qué orígenes pueden leer respuestas. No es authn, authz ni protección CSRF. Enumera orígenes exactos, métodos y cabeceras necesarios. Evita reflejar cualquier Origin; combinar credenciales con políticas comodín es especialmente arriesgado. Separa desarrollo y producción.

Rate limiting reduce fuerza bruta, scraping y sobreuso accidental, pero no arregla un endpoint costoso. Asigna cuotas a la identidad autenticada y, antes del login, a IP o clave de cliente. Inicio y recuperación de contraseña necesitan controles más estrictos. Un servicio con varias instancias requiere un contador compartido y respuestas 429 con Retry-After. Añade timeouts, paginación, límites de carga y presupuestos de concurrencia. Define cifras a partir del tráfico legítimo observado, no de un valor copiado.

Secretos, dependencias y configuración

Las credenciales no pertenecen a Git, un .env versionado, logs, tracebacks o imágenes. En producción, inyéctalas desde un gestor de secretos, concede acceso mínimo a cada carga y planifica la rotación. Impide el arranque si falta una configuración obligatoria. Si una clave se filtra, revócala: borrar el commit no elimina copias y artefactos.

Fija y revisa dependencias, mantén Python y bibliotecas en versiones soportadas y analízalas en CI. Los entornos aislados de la guía sobre venv en Python evitan conflictos, pero no sustituyen inventario y actualizaciones. Los contenedores deben ejecutarse sin root y contener solo archivos requeridos; consulta Docker con Python.

Logging útil sin información sensible

Registra eventos estructurados: hora, ruta normalizada, estado, latencia, identificador de solicitud generado por el servidor e identificador interno seudónimo. No registres por defecto cuerpos completos, Authorization, cookies, contraseñas o tokens. La censura debe ocurrir antes de que el evento salga del proceso. Define acceso, retención y alertas por fallos repetidos de login, tasas anómalas de 403 o 429 y cambios administrativos.

Un request_id permite correlacionar proxy, aplicación y base sin datos personales. No aceptes ciegamente el valor del cliente: limita formato y longitud o genera otro. El tutorial de logging en Python explica niveles y handlers.

Errores frecuentes

  • Validar un token y olvidar permisos para cada objeto.
  • Tratar CORS * como frontera de seguridad.
  • Aceptar campos desconocidos o confiar en precio, rol o propietario enviados por el cliente.
  • Aplicar un límite global que perjudica usos legítimos y resulta fácil de evadir.
  • Registrar todas las cabeceras y cuerpos para simplificar la depuración.
  • Conservar claves sin rotación o compartir una credencial entre servicios.
  • Devolver errores distintos que revelan si existe un correo o recurso.

Checklist previo al lanzamiento

  • Modela recursos, roles, scopes y denegaciones; prueba acceso horizontal y vertical.
  • Valida firma y claims, vence credenciales y permite revocación o rotación.
  • Rechaza campos extra, limita payloads y parametriza consultas.
  • Permite orígenes CORS exactos y aborda CSRF cuando las cookies autentican.
  • Aplica límites distribuidos, timeouts, paginación y topes de carga.
  • Inyecta secretos fuera del artefacto y separa acceso por servicio.
  • Censura datos sensibles y protege la retención de telemetría.
  • Automatiza casos 401, 403, 404, 422 y 429 con pytest.

La seguridad es una propiedad continua. Revisa las amenazas cuando cambien endpoints, integraciones o clases de datos, observa las señales y convierte incidentes y hallazgos en pruebas y controles repetibles.