CORS y Same-Origin Policy
Mecanismos de seguridad del navegador para controlar peticiones entre diferentes orígenes
Same-Origin Policy (SOP)
La Same-Origin Policy es una política de seguridad crítica implementada por los navegadores que restringe cómo un documento o script de un origen puede interactuar con recursos de otro origen.
¿Qué es un "Origen"?
Un origen está definido por tres componentes:
Protocolo + Dominio + Puerto https://example.com:443/path └──┬──┘ └─────┬──────┘ └┬┘ Protocolo Dominio Puerto Ejemplos: https://example.com:443 ← Origen A https://example.com:8080 ← Origen diferente (puerto distinto) http://example.com:443 ← Origen diferente (protocolo distinto) https://api.example.com ← Origen diferente (subdominio distinto)
Importante: Dos URLs tienen el MISMO origen solo si protocolo, dominio y puerto son idénticos.
Qué bloquea SOP
- • Lectura de respuestas de fetch/XMLHttpRequest cross-origin
- • Acceso al DOM de iframes de diferente origen
- • Lectura de cookies de otro dominio
- • Acceso a localStorage/sessionStorage de otro origen
Qué permite SOP
- • Cargar imágenes:
<img src="https://other.com/img.jpg"> - • Cargar scripts:
<script src="https://cdn.com/lib.js"> - • Cargar estilos:
<link href="https://cdn.com/style.css"> - • Enviar formularios:
<form action="https://other.com">
CORS - Cross-Origin Resource Sharing
CORS es un mecanismo que permite a los servidores indicar qué orígenes tienen permiso para leer sus recursos, relajando la Same-Origin Policy de manera controlada.
Flujo CORS Simple
// 1. Frontend en https://app.com hace petición
fetch('https://api.example.com/data')
.then(res => res.json())
// 2. Navegador añade header automáticamente
Origin: https://app.com
// 3. Servidor responde con headers CORS
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Credentials: true
Content-Type: application/json
{"data": "..."}
// 4. Navegador comprueba headers y permite o bloquea la respuestaPreflight Request
Para peticiones "no simples" (PUT, DELETE, custom headers), el navegador envía primero una petición OPTIONS para verificar permisos.
// 1. Navegador envía PREFLIGHT OPTIONS /api/users/123 Origin: https://app.com Access-Control-Request-Method: DELETE Access-Control-Request-Headers: Authorization // 2. Servidor responde HTTP/1.1 200 OK Access-Control-Allow-Origin: https://app.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Authorization, Content-Type Access-Control-Max-Age: 3600 // 3. Si el preflight pasa, navegador envía la petición real DELETE /api/users/123 Authorization: Bearer token123 Origin: https://app.com
Configuración CORS en el Servidor
Express.js (Node.js)
const express = require('express');
const cors = require('cors');
const app = express();
// Opción 1: Permitir TODOS los orígenes (¡INSEGURO!)
app.use(cors());
// Opción 2: Configuración específica (RECOMENDADO)
const corsOptions = {
origin: ['https://app.com', 'https://www.app.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // permitir cookies
maxAge: 3600 // cachear preflight por 1 hora
};
app.use(cors(corsOptions));
// Opción 3: Validación dinámica
app.use(cors({
origin: function (origin, callback) {
const allowedOrigins = ['https://app.com', 'https://admin.app.com'];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS no permitido'));
}
}
}));Next.js API Routes
// pages/api/data.ts
export default async function handler(req, res) {
// Configurar CORS headers manualmente
const origin = req.headers.origin;
const allowedOrigins = ['https://app.com'];
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
// Manejar preflight
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
return res.status(200).end();
}
// Manejar petición normal
res.json({ data: 'response' });
}
// O usar middleware
import Cors from 'cors';
const cors = Cors({
origin: 'https://app.com',
credentials: true,
});
function runMiddleware(req, res, fn) {
return new Promise((resolve, reject) => {
fn(req, res, (result) => {
if (result instanceof Error) return reject(result);
return resolve(result);
});
});
}
export default async function handler(req, res) {
await runMiddleware(req, res, cors);
res.json({ data: 'response' });
}Vulnerabilidades CORS
1. Wildcard con Credentials
Usar Access-Control-Allow-Origin: * conAccess-Control-Allow-Credentials: true NO está permitido.
// ❌ INVÁLIDO - El navegador lo rechazará
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// ✅ VÁLIDO - Especificar origen exacto
res.setHeader('Access-Control-Allow-Origin', 'https://app.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');2. Reflexión del Origin sin validación
Reflejar el header Origin recibido sin validar permite a cualquier sitio hacer peticiones.
// ❌ VULNERABLE
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
// ✅ SEGURO - Validar contra whitelist
const allowedOrigins = ['https://app.com', 'https://admin.app.com'];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
next();
});3. Validación de Origen débil
// ❌ VULNERABLE - Regex débil
if (origin.match(/example\.com$/)) {
// Permite: evil-example.com
// Permite: notexample.com
}
// ❌ VULNERABLE - includes() débil
if (origin.includes('example.com')) {
// Permite: https://example.com.evil.com
// Permite: https://evilexample.com
}
// ✅ SEGURO - Lista exacta
const allowedOrigins = [
'https://example.com',
'https://www.example.com',
'https://api.example.com'
];
if (allowedOrigins.includes(origin)) {
// Solo permite orígenes exactos
}Mejores Prácticas
Whitelist específica
Nunca usar * en producción, especialmente con credentials
Validar orígenes correctamente
Usar comparación exacta, no regex o includes débiles
Limitar métodos y headers
Solo permitir los métodos HTTP y headers necesarios
Usar credentials solo cuando necesario
Evitar credentials: true si no se necesitan cookies
Cachear preflight requests
Usar Access-Control-Max-Age para mejorar performance