CORS Security
CORS (Cross-Origin Resource Sharing) es un mecanismo de seguridad implementado por los navegadores modernos que controla cómo las aplicaciones web pueden realizar solicitudes HTTP a dominios diferentes de aquel que sirvió la página original, relajando de forma controlada la Same-Origin Policy (SOP): una política de seguridad fundamental que restringe que los scripts de un origen accedan a recursos de otro origen. Aunque CORS es esencial para las arquitecturas modernas de aplicaciones web, donde los front-ends frecuentemente necesitan consumir APIs alojadas en dominios diferentes, su configuración inadecuada representa una de las vulnerabilidades más comunes y peligrosas en las aplicaciones web contemporáneas. Los errores de configuración CORS pueden exponer datos sensibles a dominios no autorizados, permitir ataques de Cross-Site Request Forgery (CSRF) incluso en presencia de tokens anti-CSRF, facilitar el robo de credenciales y, en casos extremos, permitir que los atacantes ejecuten acciones privilegiadas en nombre de usuarios autenticados. El problema se agrava por el hecho de que muchos desarrolladores, al encontrar errores CORS durante el desarrollo, optan por soluciones demasiado permisivas (como usar el comodín "*" o reflejar automáticamente el origen de la solicitud) sin comprender plenamente las implicaciones de seguridad. Este artículo explora en profundidad los fundamentos de CORS, las vulnerabilidades comunes de configuración y establece prácticas robustas para una implementación segura en diferentes plataformas y frameworks, equilibrando la funcionalidad con una postura defensiva adecuada.
Same-Origin Policy (SOP)
Los navegadores implementan SOP: los scripts solo pueden acceder a recursos del mismo origen (protocolo + dominio + puerto). CORS relaja SOP de forma controlada.
# Mismo origen
https://example.com/api ← https://example.com/app [OK]
# Orígenes diferentes (bloqueado por SOP)
https://example.com ← http://example.com (protocolo)
https://example.com ← https://api.example.com (subdominio)
https://example.com ← https://example.com:8080 (puerto)
CORS Headers
Access-Control-Allow-Origin
# Permitir origen específico (recomendado)
Access-Control-Allow-Origin: https://trusted.com
# Permitir cualquier origen (¡PELIGROSO!)
Access-Control-Allow-Origin: *
# Dinámico basado en whitelist (correcto)
const allowedOrigins = ['https://app1.com', 'https://app2.com'];
const origin = request.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
Otros Headers Importantes
# Permitir credenciales (cookies, auth headers)
Access-Control-Allow-Credentials: true
# Métodos HTTP permitidos
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
# Headers permitidos en requests
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
# Headers expuestos al JavaScript del lado del cliente
Access-Control-Expose-Headers: X-Custom-Header, X-Request-Id
# Tiempo de caché del preflight (segundos)
Access-Control-Max-Age: 86400
Preflight Requests
Los navegadores envían una solicitud OPTIONS antes de las solicitudes "no simples" para verificar permisos.
# El cliente envía el preflight
OPTIONS /api/resource HTTP/1.1
Origin: https://app.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization
# El servidor responde con permisos
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400
Vulnerabilidades CORS Comunes
1. Comodín con Credenciales
# [ERROR] VULNERABLE - no funciona y es peligroso
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
# Los navegadores bloquean esta combinación
# [OK] CORRECTO - origen específico con credenciales
Access-Control-Allow-Origin: https://trusted.com
Access-Control-Allow-Credentials: true
2. Reflection Attack
# [ERROR] VULNERABLE - refleja cualquier origin
const origin = request.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
# [OK] CORRECTO - validación de whitelist
const allowedOrigins = ['https://app.com', 'https://admin.com'];
const origin = request.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
3. Subdomain Wildcard
# [ERROR] VULNERABLE - regex mal implementada
const origin = request.headers.origin;
if (/https:\/\/.*\.example\.com/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
// Acepta https://evil.example.com.attacker.com
# [OK] CORRECTO - validación estricta
const origin = request.headers.origin;
if (/^https:\/\/[a-z0-9-]+\.example\.com$/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
Configuración Segura por Tecnología
Node.js/Express (CORS middleware)
const cors = require('cors');
// Configuración segura
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400
};
app.use(cors(corsOptions));
Nginx
# Configuración condicional
map $http_origin $cors_origin {
default "";
"~^https://app\\.example\\.com$" $http_origin;
"~^https://admin\\.example\\.com$" $http_origin;
}
server {
location /api {
if ($cors_origin != "") {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
}
if ($request_method = OPTIONS) {
return 204;
}
}
}
Apache
# .htaccess
SetEnvIf Origin "^https://(app|admin)\\.example\\.com$" CORS_ORIGIN=$0
Header always set Access-Control-Allow-Origin "%e" env=CORS_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=CORS_ORIGIN
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE" env=CORS_ORIGIN
Header always set Access-Control-Allow-Headers "Authorization, Content-Type" env=CORS_ORIGIN
# Responder al preflight OPTIONS
RewriteEngine On
RewriteCond % OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
Testing CORS
# Prueba con curl
curl -H "Origin: https://evil.com" \\
-H "Access-Control-Request-Method: DELETE" \\
-H "Access-Control-Request-Headers: Authorization" \\
-X OPTIONS \\
https://api.example.com/resource
# Prueba de JavaScript
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
}).then(response => console.log(response));
Best Practices
- Nunca uses el comodín (*) en APIs con datos sensibles
- Whitelist explícita de orígenes permitidos
- Validación estricta del origen con regex segura
- Minimiza credentials: Habilítalas solo si es realmente necesario
- Least privilege: Permite solo los métodos y headers necesarios
- Cache preflight: Usa Max-Age para reducir el overhead
- Monitoring: Registra los rechazos CORS sospechosos
Checklist CORS Seguro
- [OK] Origen validado contra una whitelist explícita
- [OK] La regex de validación no permite bypasses
- [OK] Credentials habilitadas solo cuando es necesario
- [OK] Métodos y headers restringidos al mínimo
- [OK] Preflight configurado correctamente
- [OK] Probado contra orígenes maliciosos
- [OK] Logs de solicitudes rechazadas monitoreados
