CORS y seguridad de APIs DRF

Avanzado
Django
Django
Actualizado: 19/04/2026

Qué es CORS y cuándo lo necesitas

CORS (Cross-Origin Resource Sharing) es un mecanismo del navegador que restringe peticiones JS entre distintos orígenes (esquema + dominio + puerto). Sin CORS, https://miapp.com NO puede hacer fetch('https://api.miapp.com/...') (aunque comparten dominio base).

Lo necesitas cuando:

  • Tu frontend SPA vive en un dominio distinto del de la API (p. ej. https://app.miempresa.com consume https://api.miempresa.com).
  • Hay múltiples frontends (web, admin, partner) consumiendo la misma API.
  • El frontend en local (http://localhost:3000) accede a la API de desarrollo.

CORS NO es seguridad real (el servidor puede servir a cualquiera). Es un mecanismo del navegador para proteger a los usuarios. Para seguridad real, usa autenticación, HTTPS y rate limiting.

Configuración con django-cors-headers

pip install django-cors-headers
# settings.py
INSTALLED_APPS = [..., 'corsheaders']

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # DEBE estar antes de CommonMiddleware
    'django.middleware.common.CommonMiddleware',
    ...
]

# En desarrollo
CORS_ALLOWED_ORIGINS = [
    'http://localhost:3000',
    'http://localhost:5173',
    'http://localhost:8080',
]

# En producción: lista blanca estricta
CORS_ALLOWED_ORIGINS = [
    'https://app.miempresa.com',
    'https://admin.miempresa.com',
]

CORS_ALLOW_CREDENTIALS = True  # permite cookies/auth headers

Evita CORS_ALLOW_ALL_ORIGINS = True en producción. Expone tu API a cualquier sitio.

CORS con subdominios

CORS_ALLOWED_ORIGIN_REGEXES = [
    r'^https://\w+\.miempresa\.com$',
]

Permite cualquier subdominio pero solo HTTPS. Cuidado con expresiones permisivas.

Headers permitidos

Por defecto, CORS permite solo un puñado de headers. Si tu frontend envía headers custom (Authorization, X-Tenant-Id, etc.):

from corsheaders.defaults import default_headers

CORS_ALLOW_HEADERS = list(default_headers) + [
    'x-tenant-id',
    'x-api-key',
]

Preflight OPTIONS

Antes de una petición "no-simple" (PUT/DELETE, con headers custom, con cookies), el navegador envía un preflight OPTIONS. Tu API debe responder 200 con los headers permitidos. django-cors-headers lo gestiona automáticamente.

Si ves 404 en OPTIONS, revisa que el middleware esté antes de CommonMiddleware y que la URL esté registrada.

CORS vs CSRF: no confundir

  • CORS protege al usuario de peticiones cross-origin no deseadas desde un sitio malicioso que el usuario visita.
  • CSRF protege contra falsificación de petición desde un sitio que engañó al usuario a enviar una acción en tu API con su sesión.

Si tu API usa JWT en header Authorization: no necesitas CSRF. JWT se envía explícitamente por JS, no desde una form submitida desde otro sitio.

Si tu API usa cookies de sesión: necesitas CSRF. @csrf_exempt en vistas API es un agujero de seguridad salvo que uses Authorization header como CORS lo hace.

Configuración típica:

# Para APIs con JWT, no hace falta CSRF
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

Si combinas sesión + API (híbrido), usa SessionAuthentication y mantén CSRF activo:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

DRF's SessionAuthentication valida CSRF automáticamente.

HTTPS obligatorio en producción

# settings.py
SECURE_SSL_REDIRECT = True  # redirige HTTP → HTTPS
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')  # detras de reverse proxy (nginx)

# HSTS: el navegador recordara usar HTTPS durante N segundos
SECURE_HSTS_SECONDS = 31536000  # 1 ano
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True  # permite inclusion en la lista de preload de Chrome

HSTS es una declaración del servidor al navegador: "siempre usa HTTPS para mi dominio durante N tiempo". Previene downgrade attacks (forzar HTTP).

Una vez activado NO se puede bajar durante el tiempo declarado. Primero configura correctamente, luego aumenta gradualmente.

Cookies seguras

SESSION_COOKIE_SECURE = True      # cookie solo se envia por HTTPS
SESSION_COOKIE_HTTPONLY = True    # JS no puede leer la cookie (previene XSS)
SESSION_COOKIE_SAMESITE = 'Strict'  # no se envia en peticiones cross-site

CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = 'Strict'

Con SameSite=Strict, tu cookie no se envía cuando un tercero enlaza a tu dominio. Lax es más permisivo (envía en navegación GET desde links externos).

Content Security Policy

CSP (django-csp) define qué orígenes pueden cargar recursos en tu página. Previene XSS e inyección de scripts.

pip install django-csp
# settings.py
MIDDLEWARE += ['csp.middleware.CSPMiddleware']

CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", 'https://cdn.trusted.com')
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", 'https://fonts.googleapis.com')
CSP_IMG_SRC = ("'self'", 'data:', 'https:')
CSP_FONT_SRC = ("'self'", 'https://fonts.gstatic.com')
CSP_CONNECT_SRC = ("'self'", 'https://api.miempresa.com')
CSP_FRAME_ANCESTORS = ("'none'",)  # no puede ser embebida en iframe
CSP_UPGRADE_INSECURE_REQUESTS = True

Comenzar con CSP_REPORT_ONLY para ver qué se bloquearía sin bloquear:

CSP_REPORT_ONLY = True
CSP_REPORT_URI = 'https://csp-report.miempresa.com/'

Prevenir SQL injection

El ORM de Django es seguro por defecto. SIEMPRE que uses:

# SEGURO: parametrizado
Producto.objects.filter(nombre=usuario_input)
Producto.objects.raw('SELECT * FROM producto WHERE nombre = %s', [usuario_input])

NUNCA concatenar strings:

# INSEGURO: injection trivial
query = f"SELECT * FROM producto WHERE nombre = '{usuario_input}'"
cursor.execute(query)

Si necesitas SQL dinámico (nombre de columna, de tabla), valida contra lista blanca:

COLUMNAS_VALIDAS = {'nombre', 'precio', 'creado_en'}

def ordenar_por(campo):
    if campo not in COLUMNAS_VALIDAS:
        raise ValueError('Columna no permitida')
    return Producto.objects.order_by(campo)

Prevenir XSS en respuestas API

Las APIs que devuelven JSON generalmente no sufren XSS (el cliente JS controla el render). Pero si devuelves HTML como campo (p. ej. contenido de comentarios):

from django.utils.html import escape

class ComentarioSerializer(serializers.ModelSerializer):
    contenido_safe = serializers.SerializerMethodField()

    def get_contenido_safe(self, obj):
        return escape(obj.contenido)  # escapa HTML

    class Meta:
        model = Comentario
        fields = ['id', 'autor', 'contenido_safe']

Mejor sanitizar al guardar con bleach:

pip install bleach
import bleach

ALLOWED_TAGS = ['p', 'b', 'i', 'u', 'em', 'strong', 'a', 'br']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title']}

class ComentarioSerializer(serializers.ModelSerializer):
    def validate_contenido(self, value):
        return bleach.clean(value, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES, strip=True)

Protección contra DoS

Además del rate limiting (throttling de DRF), en infraestructura:

  • Cloudflare o similar como CDN/WAF: bloquea bots maliciosos antes de llegar a tu backend.
  • Límite de tamaño de body: DATA_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 (5 MB).
  • Timeouts razonables en nginx/uvicorn: 30s para requests normales, 300s máximo.
  • pgbouncer para evitar agotamiento de conexiones BBDD.

Secret management

NUNCA comitees secretos al repo:

# settings.py
import os
from dotenv import load_dotenv
load_dotenv()

SECRET_KEY = os.environ['SECRET_KEY']
DATABASE_URL = os.environ['DATABASE_URL']
AWS_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_ACCESS_KEY']

En producción, usar AWS Secrets Manager, HashiCorp Vault, o variables de entorno en el orchestrator (Kubernetes Secrets, Docker Secrets).

Rotar SECRET_KEY:

  • Invalida todas las sesiones activas.
  • Invalida todos los JWT firmados con ella.
  • Requiere re-generar todas las signed URLs de Django.

Usa una key dedicada para JWT:

SIMPLE_JWT = {
    'SIGNING_KEY': os.environ['JWT_SIGNING_KEY'],
}

Security middleware

Django incluye SecurityMiddleware con varias cabeceras automáticas. Asegura en settings:

SECURE_CONTENT_TYPE_NOSNIFF = True  # X-Content-Type-Options: nosniff
SECURE_BROWSER_XSS_FILTER = True    # X-XSS-Protection (obsoleto pero no hace daño)
X_FRAME_OPTIONS = 'DENY'            # tu app no se puede embeber en iframe
SECURE_REFERRER_POLICY = 'same-origin'

Checklist de seguridad en producción

  • [ ] DEBUG = False en producción.
  • [ ] ALLOWED_HOSTS con lista blanca exacta.
  • [ ] SECRET_KEY desde variable de entorno, no en el código.
  • [ ] HTTPS obligatorio con SECURE_SSL_REDIRECT = True.
  • [ ] HSTS activado con SECURE_HSTS_SECONDS >= 31536000.
  • [ ] SESSION_COOKIE_SECURE y CSRF_COOKIE_SECURE en True.
  • [ ] CORS con lista blanca explícita, nunca CORS_ALLOW_ALL_ORIGINS.
  • [ ] JWT con algoritmo RS256 si hay múltiples servicios.
  • [ ] Rate limiting en endpoints sensibles (login, registro).
  • [ ] Content Security Policy activo.
  • [ ] SQL injection prevenido via ORM (nunca raw SQL con concatenación).
  • [ ] XSS prevenido con escape/bleach en campos HTML de usuario.
  • [ ] Logs de acceso con user + IP + endpoint + status para auditoría.
  • [ ] Monitorización de intentos de login fallidos y 4xx anómalos.
  • [ ] Backups cifrados con rotación y tests periódicos de restore.
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Django es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Django

Explora más contenido relacionado con Django y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Instalar django-cors-headers y configurar CORS_ALLOWED_ORIGINS con la lista blanca de dominios. Diferenciar CORS de CSRF y cuando necesitas cada uno. Activar HTTPS forzado y HSTS en produccion. Configurar secure cookies con SESSION_COOKIE_SECURE y CSRF_COOKIE_SECURE. Implementar Content Security Policy con django-csp. Prevenir SQL injection usando el ORM correctamente.

Cursos que incluyen esta lección

Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje