Seguridad CORS: orígenes, cabeceras, preflight, credenciales
Cómo funciona CORS de verdad: política de mismo origen, preflight, cabeceras, credenciales, misconfiguraciones que rompen producción y las reglas de seguridad que importan.
Introducción
Todo desarrollador frontend ha visto este error alguna vez: tu app React en localhost:3000 llama a una API en localhost:8080 y el navegador se niega a entregarte la respuesta. Access to fetch has been blocked by CORS policy. La petición probablemente llegó al servidor — la ves en los logs — pero el navegador no deja que tu código lea la respuesta.
CORS (Cross-Origin Resource Sharing) es el mecanismo detrás de ese comportamiento, y está muy mal entendido. No es un sistema de autorización, no protege tu API de atacantes, y configurarlo mal puede tanto bloquear a usuarios legítimos como abrir un agujero silencioso en tu modelo de seguridad.
Esta guía cubre cómo funciona CORS de verdad — el handshake del navegador, peticiones simples vs. preflight, todas las cabeceras que importan — cómo configurarlo bien en Express y NGINX, las misconfiguraciones que lo convierten en una vulnerabilidad y cómo depurar los errores crípticos de la consola. Si llegaste aquí con un error concreto de CORS que necesitas arreglar ya, la receta Manejar CORS tiene soluciones rápidas; esta guía es para entender y asegurar el mecanismo completo.
Política de Mismo Origen (Same-Origin Policy)
Un origen es la combinación de esquema, host y puerto — cambia cualquiera de los tres y estás en un origen distinto:
https://app.example.com:443/page
│ │ │ │
esquema host puerto path
Mismo origen: https://app.example.com/otra-pagina
Origen distinto: http://app.example.com (esquema distinto)
Origen distinto: https://api.example.com (host distinto)
Origen distinto: https://app.example.com:8080 (puerto distinto)
La política de mismo origen impide que JavaScript en https://app.example.com lea respuestas que vienen de https://api.example.com. Léelo otra vez: la petición igualmente se envía — el navegador simplemente retiene la respuesta. Esa asimetría confunde constantemente: “la petición funcionaba en Postman” o “la veo en la pestaña Network, ¿por qué mi código falla?”
CORS es el mecanismo de exclusión. Un servidor que quiere compartir sus respuestas con otros orígenes devuelve cabeceras Access-Control-* diciendo qué orígenes, métodos y cabeceras acepta. Sin esas cabeceras, sin acceso — el navegador se queda la respuesta para sí.
Cómo Funciona CORS: El Handshake del Navegador
Para una petición “simple” (más abajo qué la hace simple), el navegador la envía directamente y comprueba las cabeceras CORS en la respuesta. Para cualquier otra cosa, el navegador envía primero un preflight — una petición OPTIONS preguntando “¿puedo hacer esto?” — y solo envía la petición real si el servidor dice que sí:
Dos consecuencias que vale la pena interiorizar:
- El servidor solo ve la petición real si el preflight pasó. Un preflight fallido significa que la petición real nunca se dispara — por eso un
405en OPTIONS rompe todo. - Los preflight se cachean por
Access-Control-Max-Agesegundos. Rendimiento gratis para APIs habladoras — y una trampa de depuración cuando cambias la config y el navegador sigue usando la respuesta cacheada.
Un ejemplo trabajado
Supón que tu SPA en https://app.example.com llama a fetch('https://api.example.com/users', { method: 'POST', headers: { 'Authorization': 'Bearer ...', 'Content-Type': 'application/json' }, body: '...' }). El navegador detecta un POST con un Content-Type no safelisted más una cabecera Authorization — preflight obligatorio.
Primero envía OPTIONS con Access-Control-Request-Method: POST y Access-Control-Request-Headers: authorization, content-type. Si la API responde 204 con Access-Control-Allow-Origin: https://app.example.com, Allow-Methods incluyendo POST, y Allow-Headers incluyendo tanto authorization como content-type, el navegador envía el POST real y entrega la respuesta a tu fetch. Si una sola comprobación falla, el POST nunca sale del navegador y obtienes un error CORS en consola — mientras el log del servidor muestra solo un OPTIONS solitario.
Cabeceras CORS
Cada cabecera Access-Control-* tiene una única función:
| Cabecera | Dirección | Propósito |
|---|---|---|
Access-Control-Allow-Origin | Respuesta | Qué orígenes pueden leer la respuesta |
Access-Control-Allow-Methods | Respuesta (preflight) | Qué métodos HTTP están permitidos |
Access-Control-Allow-Headers | Respuesta (preflight) | Qué cabeceras de petición están permitidas |
Access-Control-Expose-Headers | Respuesta | Qué cabeceras de respuesta puede leer JS |
Access-Control-Allow-Credentials | Respuesta | Si se pueden enviar cookies/Authorization |
Access-Control-Max-Age | Respuesta (preflight) | Cuánto tiempo cachear el preflight |
Origin | Petición | El origen que hace la petición |
Access-Control-Request-Method | Petición (preflight) | El método que el navegador quiere usar |
Access-Control-Request-Headers | Petición (preflight) | Las cabeceras que el navegador quiere enviar |
Las dos últimas las genera el navegador — las verás en los preflights, pero nunca las configuras tú.
Un detalle: Access-Control-Expose-Headers es la cabecera rara del grupo. Por defecto, el JS del navegador solo puede leer un puñado de cabeceras de respuesta “seguras” (como Content-Type). Si tu API señala paginación vía X-Total-Count o una cabecera custom de rate-limit, debes exponerla explícitamente o response.headers.get() devuelve null — sin error, solo silencio.
Peticiones Simples
Las peticiones simples saltan el preflight por completo. Una petición es “simple” solo cuando cumple todas estas condiciones:
- Método
GET,HEADoPOST - Cabeceras solo dentro del listado CORS-safelisted (
Accept,Accept-Language,Content-Language,Content-Type) Content-Typeestext/plain,multipart/form-dataoapplication/x-www-form-urlencoded
Fíjate en lo que falta: Authorization no es safelisted, y application/json no es un Content-Type safelisted. En la práctica, casi toda llamada real a una API — un POST con cuerpo JSON, o cualquier cosa con un token — no es simple y siempre hace preflight. Un GET sin cabeceras custom, o un POST de formulario clásico, son los casos típicos que sí quedan simples.
// Petición GET simple — sin preflight
GET /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
// Respuesta
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
[{"id": 1, "name": "Alice"}]
Peticiones Preflight
Para cualquier petición no simple, el navegador envía antes un OPTIONS:
// Petición preflight
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
// Respuesta preflight
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
// Petición real (enviada solo tras un preflight correcto)
POST /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGci...
{"name": "Bob", "email": "bob@example.com"}
// Respuesta real
HTTP/1.1 201 Created
Access-Control-Allow-Origin: https://app.example.com
Una respuesta preflight correcta debe devolver 204 (o 2xx) y hacer eco del método y las cabeceras permitidas. Devolver 405 Method Not Allowed en OPTIONS es la forma clásica de romper una API entera.
Configuración en el Servidor
Express.js
El paquete cors gestiona el allowlist, los preflights y el eco de cabeceras por ti. El callback origin es donde aplicas tu política:
// middleware/cors.ts — middleware CORS para Express
import cors from 'cors';
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
];
app.use(cors({
origin: (origin, callback) => {
// Permite peticiones sin origin (curl, server-to-server)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
credentials: true,
maxAge: 86400, // Cachea el preflight 24 horas
}));
La rama !origin importa: las llamadas server-to-server, curl y las apps móviles no envían cabecera Origin. Rechazarlas rompería clientes que CORS ni siquiera rige.
Cabeceras CORS manuales
Sin librería, tienes que gestionar tú mismo el camino normal y el cortocircuito de OPTIONS:
// middleware/corsManual.ts — inyección manual de cabeceras CORS
function corsMiddleware(req: Request, res: Response, next: NextFunction) {
const origin = req.headers.origin;
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
if (origin && allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin'); // Crítico para la caché
}
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Request-ID');
res.setHeader('Access-Control-Max-Age', '86400');
return res.status(204).end();
}
next();
}
NGINX
En el edge, un map mantiene el allowlist en un solo lugar y el bloque de preflight responde pronto:
# nginx.conf — configuración CORS
server {
listen 80;
# Mapa de orígenes permitidos
map $http_origin $cors_origin {
default "";
"https://app.example.com" "https://app.example.com";
"https://admin.example.com" "https://admin.example.com";
}
# Cabeceras CORS
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Vary "Origin" always;
# Manejo del preflight
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
add_header Access-Control-Max-Age 86400;
add_header Content-Length 0;
return 204;
}
}
Manejo de Credenciales
Cuando el navegador envía cookies o cabeceras Authorization — credentials: 'include' en fetch, withCredentials en XHR — las reglas se endurecen. El servidor debe responder Access-Control-Allow-Credentials: true, y el valor de Allow-Origin debe ser un origen explícito, nunca *:
// Cliente: enviar credenciales
fetch('https://api.example.com/users', {
credentials: 'include', // Enviar cookies
headers: { 'Content-Type': 'application/json' },
});
// Servidor: permitir credenciales
app.use(cors({
origin: 'https://app.example.com', // Debe ser un origen específico, NO wildcard
credentials: true,
}));
Crítico: Access-Control-Allow-Origin: * combinado con Access-Control-Allow-Credentials: true es inválido — los navegadores lo rechazan. Esta regla existe porque la combinación wildcard+credenciales dejaría a cualquier sitio hacer peticiones autenticadas en nombre del usuario, que es justo lo que la política de mismo origen existe para evitar.
CORS No Es una Frontera de Seguridad
Esta es la parte más mal entendida de todo el mecanismo, así que merece su propia sección.
Lo que CORS hace: controla si el JavaScript del navegador llega a leer una respuesta de otro origen. Protege a los usuarios de un origen frente a scripts maliciosos de otro — protege al lector, no al servidor.
Lo que CORS no hace:
- No protege tu API. curl, Postman, apps móviles y
requestsen Python no envíanOrigin— CORS simplemente no les aplica. Si tu API acepta peticiones sin autenticación, CORS no te va a salvar. - No detiene las peticiones. Un
<form>POST normal, un<img>o una redirección cruzan orígenes sin que CORS intervenga. Por eso CORS no es defensa contra CSRF — usa tokens para eso (ver Protección CSRF). - No autentica a nadie. “Tu origen está permitido” no es “tú estás autorizado” — la authn/authz del servidor sigue siendo obligatoria.
- Un CORS permisivo no hace peligroso un endpoint por sí solo.
Access-Control-Allow-Origin: *en un endpoint público, de solo lectura y sin credenciales está bien — piensa en fuentes de un CDN o una API pública. Solo es peligroso combinado con credenciales.
Cómo Elegir una Política CORS
Qué política encaja depende de quién consume la API y de si hay credenciales en juego:
| Escenario | Política | Ojo con |
|---|---|---|
| API pública de solo lectura | Allow-Origin: *, sin credenciales | No combinar con credenciales |
| App web autenticada | Allowlist de orígenes explícitos + credenciales | Nunca wildcard; lista lo más cerrada posible |
| Varios subdominios | Allowlist con coincidencia de sufijo de subdominio | evil-example.com — valida con cuidado |
| Partners terceros | Orígenes por partner en una lista gestionada | Audita quién tiene acceso con credenciales |
| Dev + prod separados | Listas de orígenes por entorno | localhost nunca debe llegar a prod |
Ante la duda, prefiere el allowlist más cerrado que mantenga tu app funcionando — y considera si un proxy mismo-origen (servir la API bajo el mismo dominio) elimina la necesidad de CORS por completo. Normalmente es la respuesta más limpia.
CORS, CSRF y CSP de un vistazo
Estos tres se confunden constantemente porque viven cerca de la frontera navegador/servidor:
| Mecanismo | Qué gobierna | Qué NO hace |
|---|---|---|
| CORS | Qué orígenes pueden leer las respuestas de tu API en un navegador | Detener peticiones, autenticar, proteger clientes no-navegador |
| Tokens CSRF | Si una petición vino de tu app real o de una falsificada | Restringir quién puede llamar a la API |
| CSP | Qué scripts, estilos y conexiones puede ejecutar o alcanzar una página | Controlar las respuestas del servidor cross-origin |
Son complementarios, no alternativas. Una app bien defendida suele ejecutar los tres: un allowlist CORS ajustado, tokens CSRF en rutas que cambian estado, y una CSP que limite a dónde podría llegar un script inyectado de todos modos. Consulta Content Security Policy y Cabeceras de Seguridad Web para los otros dos.
Misconfiguraciones Comunes
1. Origen wildcard con credenciales
// PELIGROSO: permite cualquier origen con credenciales
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
// Los navegadores rechazan esta combinación.
// Pero algunos servidores reflejan el Origin en su lugar:
Access-Control-Allow-Origin: https://evil.example.com
Access-Control-Allow-Credentials: true
// Esto es peor — deja a cualquier sitio hacer peticiones autenticadas.
2. Reflejo del origen
// PELIGROSO: refleja cualquier origen
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
// Cualquier sitio web puede hacer peticiones cross-origin autenticadas.
El reflejo salta el filtro del wildcard devolviendo el propio origen del atacante — es la forma más común de que una configuración CORS real se vuelva explotable. Los write-ups de bug bounty sobre misconfiguraciones CORS casi siempre resultan ser alguna variante de “el servidor refleja Origin sin comprobarlo”.
3. Origen null
// PELIGROSO: el origen null lo usan los iframes en sandbox y las data: URIs
Access-Control-Allow-Origin: null
// Un atacante puede usar <iframe sandbox="allow-scripts"> para enviar peticiones desde el origen null.
4. Orígenes regex mal hechos
Los patrones tipo *.example.com son cómodos, pero un matching descuidado (endsWith('example.com') sin el punto) deja pasar a evil-example.com o example.com.attacker.com. Valida el hostname parseado, no un sufijo de string.
Patrones para Producción
Validación dinámica de orígenes
// middleware/corsSecure.ts — validación dinámica segura de orígenes
const ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://admin.example.com',
'https://staging.example.com',
]);
function isAllowedOrigin(origin: string): boolean {
if (ALLOWED_ORIGINS.has(origin)) return true;
// Cualquier subdominio *.example.com — parsea el hostname, no strings
const url = new URL(origin);
return url.hostname === 'example.com' || url.hostname.endsWith('.example.com');
}
app.use(cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true); // Server-to-server
if (isAllowedOrigin(origin)) {
callback(null, true);
} else {
callback(new Error(`Origin ${origin} not allowed`));
}
},
credentials: true,
}));
Proxy mismo-origen como alternativa
Cuando frontend y API están bajo tu control, un proxy mismo-origen suele ganar a CORS por completo: sirve https://app.example.com/api/* y deja que tu web server o edge function reenvíe al backend real. El navegador ve un solo origen, sin preflights, sin cabeceras que configurar mal — y tus cookies siguen siendo first-party. El coste es algo más de infra: ahora mantienes un salto de reenvío en la ruta de las peticiones.
CORS por ruta
Endpoints distintos pueden justificar políticas distintas — un health check público vs una API de usuarios autenticada:
// CORS estricto para endpoints sensibles, relajado para API pública
app.get('/api/public/health', cors({ origin: '*' }), healthCheck);
app.post('/api/users', cors({
origin: 'https://app.example.com',
credentials: true,
}), createUser);
app.delete('/api/users/:id', cors({
origin: 'https://admin.example.com',
credentials: true,
}), deleteUser);
Depuración de Errores CORS
Los errores de consola son crípticos pero se mapean a un conjunto pequeño de causas:
| Error de consola | Causa real | Solución |
|---|---|---|
blocked by CORS policy: No 'Access-Control-Allow-Origin' header | El servidor no envió la cabecera, o la petición falló antes del middleware CORS | Revisa el preflight/respuesta en DevTools — cabecera ausente, 500 o 4xx upstream |
The value of the 'Access-Control-Allow-Origin' header must not be '*' | Wildcard combinado con credentials: 'include' | Haz eco del origen específico permitido |
Origin ... is not allowed by Access-Control-Allow-Origin | Tu origen no está en el allowlist | Añádelo, o revisa un desajuste dev/staging/prod |
Method POST is not allowed by Access-Control-Allow-Methods | El método falta en la lista del preflight | Añade el método a allowedMethods |
header 'authorization' is not allowed | La cabecera Authorization falta en Allow-Headers | Añádela a allowedHeaders |
Response to preflight doesn't pass access control check | El preflight devolvió un error (405/500) | Tu manejador de OPTIONS está roto — devuelve 204 con las cabeceras |
| Funciona en Postman, falla en el navegador | Postman no envía Origin — CORS no le aplica | Esperado; depura desde el lado del navegador |
El bucle de depuración que sí funciona: DevTools → Network → localiza la petición OPTIONS → revisa su status y sus cabeceras de respuesta → compáralas con lo que tu config dice que envía. Nueve de cada diez veces, la respuesta del preflight está perdiendo exactamente la cabecera que nombra el error.
Si estás detrás de un CDN o reverse proxy, comprueba también que no elimine ni pise tus cabeceras Access-Control-* — algunos proxies gestionados tienen su propia capa CORS que choca en silencio con las de tu aplicación. Ante la duda, pega al backend directamente con curl -H 'Origin: https://app.example.com' -X OPTIONS -i para aislar si la pérdida ocurre en tu app o en un intermediario.
Una palabra sobre Vary: si el CDN o el navegador cachea una respuesta ajustada a un origen y la sirve a otro, obtienes errores CORS intermitentes y misteriosos. Envía siempre Vary: Origin cuando Allow-Origin dependa del origen de la petición.
Buenas Prácticas
- Nunca uses
Access-Control-Allow-Origin: *con credenciales — los navegadores lo bloquean, y el workaround (reflejo) es una vulnerabilidad - Usa un allowlist de orígenes específicos — no reflejes la cabecera Origin a ciegas
- Configura
Vary: Origin— evita envenenamiento de caché cuando las respuestas dependen del origen - Cachea el preflight con
Access-Control-Max-Age— reduce las peticiones OPTIONS (86400 segundos es habitual) - Permite solo los métodos necesarios — no permitas todos los métodos HTTP por defecto
- Permite solo las cabeceras necesarias — no permitas todas las cabeceras de petición
- Expón solo las cabeceras de respuesta necesarias — no expongas cabeceras internas
- Maneja el preflight OPTIONS explícitamente — devuelve 204 con las cabeceras correctas
- No permitas el origen
null— los iframes en sandbox lo usan y se puede explotar - Prefiere un proxy mismo-origen cuando elimine por completo la necesidad de cross-origin
- Prueba con DevTools del navegador — revisa la pestaña Network buscando el preflight y errores CORS
Para defensa en profundidad más allá de CORS, consulta Content Security Policy: CSP Headers, Nonces, Hashes y Cabeceras de Seguridad Web.
Errores Comunes
- Reflejar Origin sin comprobar: cualquier sitio puede entonces llamar a tu API cross-origin. Verifica siempre contra un allowlist.
- Cabecera
Vary: Originausente: el CDN cachea una respuesta para un origen y la sirve a otro. Causa errores CORS o problemas de seguridad. - Wildcard + credenciales:
*junto acredentials: trueno funciona — los navegadores lo rechazan de entrada. - No manejar OPTIONS: las peticiones preflight necesitan un 204 con cabeceras CORS. Devolver 405 bloquea todas las peticiones no simples.
- Permitir todas las cabeceras:
Access-Control-Allow-Headers: *abarca más de lo necesario — lista solo las que usa tu app. - Olvidar que Content-Type dispara preflight:
application/jsonno es safelisted, así que un POST con JSON siempre hace preflight. - Asumir que CORS protege la API: solo controla la lectura en el navegador. Los clientes no-navegador lo saltan por completo.
Ver también
- MDN: Cross-Origin Resource Sharing (CORS) — la referencia sobre cabeceras, preflight y comportamiento del navegador
- Manejar CORS — soluciones rápidas para los errores de consola comunes
- Protección CSRF — el ataque contra el que CORS no defiende
- Cabeceras de Seguridad Web — el panorama completo de cabeceras de seguridad
Preguntas frecuentes
¿Qué es CORS?
Cross-Origin Resource Sharing — un mecanismo del navegador que deja a JavaScript en un origen leer respuestas de otro origen, cuando el servidor lo permite vía cabeceras Access-Control-*. Sin CORS, la política de mismo origen bloquea esas lecturas por defecto.
¿Qué es una petición preflight?
Una petición OPTIONS que el navegador dispara antes de la real cuando una petición no es "simple" — POST con JSON, PUT, DELETE, o cualquier cosa con cabeceras como Authorization. El navegador pregunta al servidor si el método, las cabeceras y el origen están permitidos, y envía la petición real solo si la respuesta es sí.
¿Cuál es la diferencia entre peticiones simples y con preflight?
Las peticiones simples — GET/HEAD/POST con solo cabeceras safelisted y cuerpos text/plain, multipart/form-data o application/x-www-form-urlencoded — van directas y el navegador comprueba CORS en la respuesta. Las peticiones con preflight envían un OPTIONS de comprobación primero, y la petición real solo si el preflight pasa.
¿Puede CORS prevenir CSRF?
No. CORS controla si el JavaScript del navegador puede leer respuestas de otro origen. Los ataques CSRF no necesitan leer nada — un <form> o un <img> cruzan orígenes sin que CORS intervenga. La protección contra CSRF requiere tokens o cookies SameSite; ver Protección CSRF.
¿Por qué me da un error CORS en producción pero no en desarrollo?
En dev, el frontend y la API suelen compartir localhost, a menudo a través de un proxy, así que no hay cross-origin en absoluto. En producción viven en dominios distintos (app.example.com vs api.example.com), que es cuando entran las reglas CORS. Configura CORS en el servidor de la API para el origen exacto de producción — y no dejes localhost en el allowlist de producción.
¿Por qué la petición funciona en Postman pero falla en el navegador?
Postman, curl y el código server-to-server no envían cabecera Origin, así que CORS nunca les aplica — es un mecanismo solo del navegador, aplicado por el navegador y no por el servidor. Una petición que funciona fuera del navegador demuestra que el endpoint está bien; el problema vive en la configuración CORS, no en tu API. Y si necesitas restringir el acceso server-to-server, usa autenticación, reglas de red o un API gateway — no cabeceras CORS.
¿Puedo usar Access-Control-Allow-Origin: * con credenciales?
No — los navegadores rechazan explícitamente esa combinación. Si necesitas cookies o cabeceras Authorization cross-origin, debes hacer eco del origen específico permitido y enviar Access-Control-Allow-Credentials: true. Reflejar orígenes arbitrarios para saltarse esto es una vulnerabilidad real, no una solución.
¿Es peligroso Access-Control-Allow-Origin: null?
Sí. El origen null es lo que usan los iframes en sandbox, las URLs data: y los archivos locales. Permitirlo significa que un atacante puede meter un iframe en sandbox en cualquier página y hacer peticiones en nombre de tus usuarios. Valida siempre contra orígenes explícitos — nunca permitas null.
¿Cómo evito que una config CORS vieja quede cacheada?
Los resultados del preflight se cachean en el navegador durante Access-Control-Max-Age segundos, así que un arreglo puede parecer que "no funciona" mientras el navegador conserva la respuesta vieja. Limpia la caché del sitio, abre una ventana de incógnito, o baja temporalmente Max-Age mientras depuras. En el lado del CDN, Vary: Origin mantiene las respuestas cacheadas acotadas a su origen en lugar de filtrarse a otros.
Recursos Relacionados
Web Security Headers
Implementa CSP, HSTS, X-Frame-Options y headers seguros. Cubre content security policy, CORS, referrer policy, permissions policy y testing con security scanners.
GuideContent Security Policy: cabeceras CSP, nonces, hashes
Desplegá Content Security Policy sin romper tu sitio: directivas CSP, nonces, hashes, strict-dynamic, reportes e integración por framework.
RecipeProteger Formularios Web Contra Ataques CSRF
Cómo prevenir ataques de Cross-Site Request Forgery usando tokens de sincronización, cookies SameSite y patrones de double-submit cookie.
RecipeConfigurar CORS en Express, Flask y Spring Boot
Cómo configurar los encabezados de Cross-Origin Resource Sharing (CORS) correctamente para APIs, SPAs y funciones serverless sin abrir agujeros de seguridad.
GuideOAuth2 y OIDC: Authorization Code, PKCE, Token Validation
Dominá OAuth2 y OpenID Connect para producción: authorization code flow con PKCE, token validation, refresh tokens, scopes, JWT verification y security best practices.