Módulo 5: Security Headers y Sessions — Introducción
Cápsula 02: Security headers
Descripción
Los security headers son instrucciones que tu API envía al cliente diciéndole "trátame de esta forma específica para protegerme contra estos ataques". El browser los respeta automáticamente. Apps con headers correctos previenen XSS, clickjacking, downgrade attacks, MIME sniffing.
En esta cápsula vas a implementar middleware FastAPI que aplica los headers correctos a TODAS las responses, vas a entender qué ataque previene cada header, y vas a aprender por qué algunos (como CSP) son distintos para APIs vs web apps.
Los headers críticos
| Header | Para qué |
|---|---|
| Strict-Transport-Security (HSTS) | Forzar HTTPS, prevenir downgrade attacks |
| X-Content-Type-Options | Prevenir MIME sniffing |
| X-Frame-Options | Prevenir clickjacking |
| Content-Security-Policy (CSP) | Controlar qué recursos puede cargar el browser |
| Referrer-Policy | Controlar qué info de referer se manda |
| Permissions-Policy | Controlar acceso a APIs del browser (camera, geolocation) |
Vamos uno por uno.
1. Strict-Transport-Security (HSTS)
Header:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Qué hace: le dice al browser "siempre usa HTTPS para este dominio durante el próximo año, para todos los subdominios".
Qué previene: downgrade attacks. Sin HSTS, un atacante en la red puede interceptar la primera request HTTP del usuario y hacerle creer que el sitio no soporta HTTPS. Con HSTS, el browser nunca acepta HTTP después de la primera vez.
Componentes:
max-age=31536000— 1 año en segundos (recomendación OWASP)includeSubDomains— aplica a *.tudominio.com tambiénpreload— opcional; permite que el browser cargue HSTS antes de la primera visita (requiere submit a hstspreload.org)
SOLO en HTTPS. En HTTP el header se ignora. NO sirve si tu API está en HTTP.
2. X-Content-Type-Options
Header:
X-Content-Type-Options: nosniff
Qué hace: previene que el browser intente "adivinar" el MIME type del response (lo respeta tal cual lo declaras).
Qué previene: un atacante sube un archivo .txt que en realidad contiene JavaScript. Sin nosniff, algunos browsers lo ejecutan como JS. Con nosniff, lo tratan estrictamente como texto.
Para APIs JSON: importante porque previene que un response JSON sea interpretado como HTML/JS si tu Content-Type tiene un typo.
Configuración: un solo valor nosniff. Aplica siempre, sin trade-offs.
3. X-Frame-Options
Header:
X-Frame-Options: DENY
Qué hace: previene que tu sitio sea cargado en un <iframe>.
Qué previene: clickjacking. Un atacante carga tu app en un iframe invisible, superpone elementos para que el usuario haga click sin saber.
Valores:
DENY— nunca puede ser embebido en iframeSAMEORIGIN— solo del mismo dominioALLOW-FROM uri— DEPRECATED, no usar
Para APIs: las APIs no deberían cargarse en iframes nunca. DENY es la opción correcta.
Note: modernamente reemplazado por Content-Security-Policy: frame-ancestors. Mantener X-Frame-Options para compat con browsers viejos.
4. Content-Security-Policy (CSP)
Header (para una API JSON):
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
Qué hace: controla qué recursos (scripts, imágenes, frames, etc.) el browser puede cargar al renderizar tu response.
Qué previene: XSS (cross-site scripting). Si un atacante logra inyectar <script> en tu response, CSP lo bloquea antes de ejecutarse.
Para APIs JSON específicamente:
default-src 'none' ← no permitir cargar NADA por default
frame-ancestors 'none' ← no permitir embedding en iframes
Como tu API solo retorna JSON, no necesitas permitir scripts/images/styles. default-src 'none' es lo más restrictivo y correcto.
Para APIs que también sirven HTML (ej. /docs de Swagger):
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net
(/docs requiere CDN de Swagger UI, lo permites explícitamente)
5. Referrer-Policy
Header:
Referrer-Policy: strict-origin-when-cross-origin
Qué hace: controla qué información del Referer header se manda en requests subsecuentes.
Por qué importa: sin esto, si tu API está en api.myapp.com/users/42, el browser puede mandar esa URL completa como Referer a sitios externos. La URL puede contener info sensible (IDs, tokens query string, etc.).
Valores comunes:
no-referrer— nunca mandar Refererstrict-origin-when-cross-origin— solo el origin (no el path), y solo cuando va al mismo originsame-origin— solo a recursos del mismo origin
Recomendado: strict-origin-when-cross-origin para APIs.
6. Permissions-Policy (opcional pero recomendado)
Header:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Qué hace: restringe qué APIs del browser pueden ser usadas.
Para APIs JSON: las APIs del browser no aplican a tu response. Lo dejas restrictivo (todas vacías) para defensa en profundidad por si tu response alguna vez sirve HTML.
Implementación con FastAPI middleware
# app/middleware/security_headers.py
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
SECURITY_HEADERS = {
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Content-Security-Policy": "default-src 'none'; frame-ancestors 'none'",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=()",
}
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
"""Agrega security headers a cada response."""
async def dispatch(self, request: Request, call_next) -> Response:
response = await call_next(request)
for header, value in SECURITY_HEADERS.items():
response.headers[header] = value
return response
Registrar en main.py:
# app/main.py
from app.middleware.security_headers import SecurityHeadersMiddleware
app = FastAPI(...)
app.add_middleware(SecurityHeadersMiddleware)
Excepción: /docs y /redoc necesitan CSP relajado
Swagger UI carga assets de CDN. Si aplicas default-src 'none' a /docs, no carga.
Solución: detectar la ruta y aplicar CSP distinto:
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
response = await call_next(request)
for header, value in SECURITY_HEADERS.items():
if header == "Content-Security-Policy" and request.url.path in ("/docs", "/redoc", "/openapi.json"):
# CSP relajado para Swagger/ReDoc UI
response.headers[header] = (
"default-src 'self' https://cdn.jsdelivr.net; "
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
"img-src 'self' data: https://cdn.jsdelivr.net; "
"font-src 'self' https://cdn.jsdelivr.net"
)
else:
response.headers[header] = value
return response
Verificar headers funcionan
curl -I http://localhost:8000/api/v1/posts
HTTP/1.1 200 OK
date: ...
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff
x-frame-options: DENY
content-security-policy: default-src 'none'; frame-ancestors 'none'
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=(), payment=()
content-type: application/json
Y con tools:
# securityheaders.com (online)
# Cargar tu URL pública
# Mozilla Observatory
# observatory.mozilla.org → analyze
# Localmente con curl
curl -I -s https://api.myapp.com | grep -i 'strict\|frame\|content-security'
Test del middleware
# tests/test_security_headers.py
import pytest
@pytest.mark.asyncio
@pytest.mark.parametrize("header", [
"strict-transport-security",
"x-content-type-options",
"x-frame-options",
"content-security-policy",
"referrer-policy",
"permissions-policy",
])
async def test_security_headers_present(client, header):
response = await client.get("/api/v1/posts")
assert header in response.headers
@pytest.mark.asyncio
async def test_x_frame_options_deny(client):
response = await client.get("/api/v1/posts")
assert response.headers["x-frame-options"] == "DENY"
@pytest.mark.asyncio
async def test_x_content_type_options_nosniff(client):
response = await client.get("/api/v1/posts")
assert response.headers["x-content-type-options"] == "nosniff"
@pytest.mark.asyncio
async def test_hsts_at_least_one_year(client):
response = await client.get("/api/v1/posts")
hsts = response.headers["strict-transport-security"]
# max-age debe ser al menos 1 año (31536000)
assert "max-age=" in hsts
max_age = int(hsts.split("max-age=")[1].split(";")[0])
assert max_age >= 31536000
Outputs esperados
# Antes (sin middleware)
$ curl -I http://localhost:8000/api/v1/posts
HTTP/1.1 200 OK
content-type: application/json
content-length: 2
# Después (con middleware)
$ curl -I http://localhost:8000/api/v1/posts
HTTP/1.1 200 OK
content-type: application/json
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff
x-frame-options: DENY
content-security-policy: default-src 'none'; frame-ancestors 'none'
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=(), payment=()
Audit con securityheaders.com
Si tu API es pública:
- Visita https://securityheaders.com
- Pega tu URL
- Recibes grade A+ a F
Con los headers que configuramos: A o A+.
Troubleshooting
Problema 1: HSTS no aparece en navegador
HSTS solo es respetado en HTTPS. En desarrollo local (HTTP) el header se manda pero el browser lo ignora.
Problema 2: CSP rompe Swagger UI
Aplicar CSP relajado para /docs y /redoc. Código mostrado arriba.
Problema 3: X-Frame-Options DENY rompe embedding intencional
Si tu API debería ser embebible (raro), cambia a SAMEORIGIN o usa Content-Security-Policy: frame-ancestors 'self'.
Problema 4: Headers no se aplican
Verifica que el middleware está registrado ANTES de las rutas:
app = FastAPI(...)
app.add_middleware(SecurityHeadersMiddleware) # ← ANTES de include_router
app.include_router(...)
Problema 5: Necesito CSP que permite cierto dominio
Construir el header dinámicamente:
ALLOWED_CDN = "https://cdn.myapp.com"
csp = f"default-src 'self'; script-src 'self' {ALLOWED_CDN}"
Ejercicios
Ejercicio 1: Implementa el middleware
Crea app/middleware/security_headers.py y registra en main.py. Verifica con curl que los headers aparecen.
Ver solución
Código en la cápsula. Verificar:
curl -I http://localhost:8000/api/v1/posts | grep -i 'strict\|frame\|content-security'
Ejercicio 2: Tests de cada header
Implementa los tests parametrize de la cápsula. Confirma que todos pasan.
Ver solución
Código en la cápsula.
Ejercicio 3: CSP relajado para /docs
Implementa la lógica para aplicar CSP distinto en /docs. Verifica que /docs sigue funcionando con el browser.
Ver solución
Código en la cápsula. Visitar /docs en browser → debería renderizar correctamente. Sin la excepción, /docs aparece roto (sin estilos).
Ejercicio 4: Audit con tool externa
Si tu API tiene URL pública (ngrok, Render, Railway), corre:
- securityheaders.com
- observatory.mozilla.org
Reporta el grade obtenido.
Ver solución
Con los headers configurados, esperado: A+ en securityheaders.com, A+ en Mozilla Observatory.
Si recibes grado más bajo, los reportes te dicen qué falta.
Ejercicio 5: Detecta el bug
class BadMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["X-Frame-Options"] = "ALLOW-ALL"
return response
¿Qué problema tiene?
Ver solución
ALLOW-ALL no es un valor válido de X-Frame-Options. Browsers lo ignoran. Como resultado, NO hay protección contra clickjacking.
Valores válidos: DENY, SAMEORIGIN. Para "permitir todo", simplemente no setear el header (o usar CSP frame-ancestors *).
Resumen
- Security headers son defensa de capa browser. El browser los respeta y previene ataques comunes.
- HSTS previene downgrade attacks. Solo activo en HTTPS.
max-age=31536000(1 año). - X-Content-Type-Options: nosniff previene MIME sniffing.
- X-Frame-Options: DENY previene clickjacking.
- CSP controla qué recursos cargar. Para APIs JSON:
default-src 'none'. - Referrer-Policy controla qué info de referer se filtra.
- Permissions-Policy restringe APIs del browser.
- Middleware FastAPI aplica headers a TODAS las responses.
- /docs y /redoc necesitan CSP relajado para cargar Swagger UI.
- Auditar con securityheaders.com y Mozilla Observatory.
Recursos adicionales
- OWASP — Secure Headers Project — Lista exhaustiva
- MDN — HTTP Security Headers — Doc oficial
- securityheaders.com — Tool de audit
- Mozilla Observatory — Audit comprehensivo
- HSTS Preload — Submit tu dominio para preload
- CSP Generator — Helper para construir CSP
Siguiente paso
Cápsula 03: API key authentication. Para integraciones service-to-service que NO usan OAuth/JWT. Vas a implementar API keys con hash, scopes, y validación.