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.comconsumehttps://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: sí 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 = Falseen producción. - [ ]
ALLOWED_HOSTScon lista blanca exacta. - [ ]
SECRET_KEYdesde variable de entorno, no en el código. - [ ] HTTPS obligatorio con
SECURE_SSL_REDIRECT = True. - [ ] HSTS activado con
SECURE_HSTS_SECONDS >= 31536000. - [ ]
SESSION_COOKIE_SECUREyCSRF_COOKIE_SECUREen 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
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