Wiki/Fundamentos/CORS y Same-Origin Policy
Principiante
16 min lectura

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 respuesta

Preflight 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

Recursos Adicionales

Siguiente Paso

Aprende sobre vulnerabilidades comunes en aplicaciones web

Cross-Site Scripting (XSS)