● CWE-942 · OWASP A05:2021

Configuración incorrecta de CORS (CWE-942): cómo el reflejo de Origin filtra datos y cómo corregirlo

Una API que responde a cualquier Origin con Access-Control-Allow-Origin igual a ese origen y Access-Control-Allow-Credentials: true permite que cualquier sitio web lea sus respuestas con las cookies del visitante. Cómo las comprobaciones de sufijo, el origen null y los orígenes reflejados rompen la política del mismo origen, y la lista de permitidos con coincidencia exacta que lo corrige, con un ejemplo en Express y cors.

Explicación en Lenguaje Sencillo (ELI5)

Un cajero de banco lee tu saldo en voz alta a quien tú envíes, siempre que el mensajero muestre una carta que diga para quién trabaja y la lista del cajero apruebe ese nombre. La regla de un cajero es 'aprobar cualquier nombre que termine en la palabra García'. Un desconocido envía un mensajero con una carta de 'Pérez García', el cajero la aprueba y tu saldo se lee a alguien a quien nunca enviaste. El reflejo de Origin es la versión perezosa en la que el cajero aprueba todos los nombres. La solución es una lista corta y exacta de nombres, comparados letra por letra.

Conceptos Clave y Términos

Política del mismo origen
Los navegadores solo permiten que un script lea respuestas de su propio origen (esquema, host y puerto). Las cabeceras de respuesta CORS son la forma en que un servidor relaja esa regla a propósito para orígenes concretos.
Access-Control-Allow-Origin con credenciales
Si la respuesta nombra el origen de quien llama en Access-Control-Allow-Origin y envía Access-Control-Allow-Credentials: true, un script en ese origen puede leer respuestas a peticiones que llevaron las cookies del usuario. Los navegadores rechazan * junto con credenciales, por eso los servidores mal configurados devuelven el origen tal cual.
Reflejo de Origin y comprobación de sufijo
Copiar la cabecera Origin en la respuesta confía en todos los sitios. Comprobaciones como endsWith('example.com'), búsquedas de subcadenas o expresiones regulares sin anclas y con puntos sin escapar también aceptan https://attacker-example.com.
El origen null
Los iframes en sandbox, las URL data: y algunas redirecciones envían Origin: null. Cualquier atacante puede producirlo, así que permitir null equivale a permitir a todo el mundo.
Vary: Origin
Cuando Access-Control-Allow-Origin depende de la petición, la respuesta debe llevar Vary: Origin para que las cachés compartidas no sirvan a un origen las cabeceras CORS de otro. El paquete cors la añade para orígenes dinámicos.

Flujo de Ataque Paso a Paso

Paso 1

La víctima tiene sesión en el sitio de la API

Su navegador guarda una cookie de sesión de api.example.com que se envía en peticiones entre sitios.

Paso 2

Abre una página en el dominio del atacante

Un script llama a fetch('https://api.example.com/api/account', { credentials: 'include' }). Una página en https://attacker-example.com que solo muestra en la consola la longitud de la respuesta ilustra el caso.

Paso 3

La API aprueba el origen

La comprobación de sufijo acepta attacker-example.com, así que la respuesta lleva ese origen en Access-Control-Allow-Origin junto con Access-Control-Allow-Credentials: true.

Paso 4

El script lee datos privados

El navegador entrega el JSON de la cuenta al script del atacante, que puede enviarlo a cualquier parte. En 2016 James Kettle encontró este patrón en varios exchanges de Bitcoin, donde exponía claves de API y datos de monederos.

Código Fuente: Vulnerable vs. Seguro

IMPLEMENTACIÓN VULNERABLE
// server.js: cualquier origen que termine en example.com, más null, puede leer con cookies
import express from 'express';
import cors from 'cors';
import { accountFor, requireSession } from './account.js';

const app = express();

// true hace que cors devuelva el Origin de quien llama junto con credenciales
app.use(cors({
  origin: (origin, callback) => callback(null, !origin || origin === 'null' || origin.endsWith('example.com')),
  credentials: true,
}));

// datos privados de la cuenta legibles por los orígenes aprobados
app.get('/api/account', requireSession, (req, res) => {
  res.json(accountFor(req.session.userId));
});
PARCHE SEGURO Y ROBUSTO
// server.js: dos orígenes exactos, nada más
import express from 'express';
import cors from 'cors';
import { accountFor, requireSession } from './account.js';

const app = express();

// orígenes completos con esquema y host, comparados exactamente
const ALLOWED_ORIGINS = new Set(['https://app.example.com', 'https://admin.example.com']);

// un Origin desconocido o ausente no recibe cabeceras CORS; cors añade Vary: Origin
app.use(cors({
  origin: (origin, callback) => callback(null, origin !== undefined && ALLOWED_ORIGINS.has(origin)),
  credentials: true,
  methods: ['GET', 'POST'],
  maxAge: 600,
}));

// los datos personales nunca se guardan en cachés compartidas
app.get('/api/account', requireSession, (req, res) => {
  res.set('Cache-Control', 'no-store');
  res.json(accountFor(req.session.userId));
});

Lista de Verificación de Seguridad para Ingeniería

Fuentes

← Ver el directorio completo de seguridad Todas las guías de vulnerabilidades →