Visión General
Antes de que un origen pueda leer la respuesta de otro, el navegador consulta CORS (Cross-Origin Resource Sharing). Un CORS mal configurado es una de las fuentes más habituales de fricción entre frontend y backend, y también de agujeros de seguridad reales. Lo que sigue configura el middleware de CORS como toca: validación de la lista de orígenes, manejo del preflight, credenciales y declaración explícita de métodos y encabezados, en Python, JavaScript y Java.
Cuándo Usar
Usa esta receta cuando:
- Tu frontend (una SPA, app móvil o widget de terceros) corre en un origen distinto al de tu API.
- Necesitas aceptar peticiones cross-origin autenticadas con cookies o encabezados de autorización.
- Construyes una API pública consumida por dominios externos.
- Persigues un críptico error “blocked by CORS policy” en la consola del navegador.
Solución
Versiones ejecutables de estos ejemplos, más un script curl que verifica los encabezados, están en el repositorio complementario.
Python (Flask)
from flask import Flask, request, make_response
app = Flask(__name__)
ALLOWED_ORIGINS = {
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
}
ALLOWED_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"]
ALLOWED_HEADERS = ["Content-Type", "Authorization", "X-Request-ID"]
ALLOW_CREDENTIALS = True
@app.after_request
def add_cors_headers(response):
origin = request.headers.get("Origin")
# Solo refleja orígenes permitidos; nunca uses "*" con credenciales
if origin in ALLOWED_ORIGINS:
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Vary"] = "Origin"
if ALLOW_CREDENTIALS:
response.headers["Access-Control-Allow-Credentials"] = "true"
return response
@app.route("/api/<path:path>", methods=["OPTIONS"])
def handle_preflight(path):
origin = request.headers.get("Origin")
if origin not in ALLOWED_ORIGINS:
return make_response(("", 204)) # Sin headers CORS para orígenes no permitidos
response = make_response("", 204)
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Methods"] = ", ".join(ALLOWED_METHODS)
response.headers["Access-Control-Allow-Headers"] = ", ".join(ALLOWED_HEADERS)
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Max-Age"] = "86400"
return response
JavaScript (Express)
import express from "express";
const app = express();
const ALLOWED_ORIGINS = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
]);
function corsMiddleware(req, res, next) {
const origin = req.headers.origin;
if (origin && ALLOWED_ORIGINS.has(origin)) {
res.header("Access-Control-Allow-Origin", origin);
res.header("Vary", "Origin");
}
res.header("Access-Control-Allow-Credentials", "true");
// Petición de preflight
if (req.method === "OPTIONS") {
if (origin && !ALLOWED_ORIGINS.has(origin)) {
return res.sendStatus(204);
}
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH");
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-ID");
res.header("Access-Control-Max-Age", "86400");
return res.sendStatus(204);
}
next();
}
app.use(corsMiddleware);
app.use(express.json());
// Alternativa: el paquete cors con lista explícita de orígenes
// import cors from "cors";
// app.use(cors({
// origin: (origin, callback) => {
// if (!origin || ALLOWED_ORIGINS.has(origin)) {
// callback(null, true);
// } else {
// callback(new Error("Not allowed by CORS"));
// }
// },
// credentials: true,
// methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
// allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
// }));
Java (Spring Boot)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig {
private static final String[] ALLOWED_ORIGINS = {
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000"
};
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(ALLOWED_ORIGINS)
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
.allowedHeaders("Content-Type", "Authorization", "X-Request-ID")
.allowCredentials(true)
.maxAge(86400);
}
};
}
}
// Integración con Spring Security (si usas SecurityFilterChain)
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors(cors -> {})
.csrf(csrf -> csrf.disable()) // solo si la API es stateless con tokens
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
);
// Para patrones de autenticación, consulta la guía de seguridad de APIs de StackPractices.
return http.build();
}
}
Explicación
- Same-Origin Policy: los navegadores bloquean por defecto las peticiones de
origin-a.comaorigin-b.com, y CORS es la forma controlada de relajar esa política. - Preflight (OPTIONS): el navegador manda una petición de preflight antes de los métodos no simples (PUT, DELETE, PATCH) o encabezados personalizados, y el servidor tiene que responder con los orígenes, métodos y encabezados permitidos.
Access-Control-Allow-Originquiere un origen literal (https://app.example.com), y el único atajo disponible es*. Nunca combines*conAccess-Control-Allow-Credentials: true, porque el navegador rechaza esa combinación directamente.- Recurre a
Vary: Originsiempre que los encabezados CORS dependan de quién pregunta. Sin él, un CDN puede cachear una respuesta con el encabezado de un origen y servirla a peticiones de otro. Access-Control-Allow-Credentialses el interruptor que deja viajar a las cookies y a los encabezadosAuthorizationen peticiones cross-origin. Tienen que activarlo tanto el cliente (credentials: 'include'en fetch,withCredentials: trueen axios) como el servidor.
La secuencia de la petición queda así:
El preflight solo ocurre en peticiones no simples. Un GET plano con encabezados estándar lo salta y va directo a la llamada real.
Variantes
| Enfoque | Configuración | Ideal para |
|---|---|---|
| Lista de orígenes | Lista explícita de orígenes | APIs de producción con consumidores conocidos |
| Patrón regex | *.example.com | Comodines de subdominio (valida con cuidado) |
| Origen dinámico | Origen validado en cada petición | APIs multi-tenant con orígenes por tenant |
Comodín * | Sin restricción de origen | APIs públicas de solo lectura sin credenciales |
| Proxy | El frontend proxyea a la API | Desarrollo y despliegues en el mismo origen |
Lo Que Funciona
- Nunca uses
*con credenciales: el navegador rechazaAccess-Control-Allow-Origin: *en cuanto apareceAllow-Credentials: true, así que refleja el origen de la petición tras comprobarlo contra tu lista. - Valida los orígenes explícitamente: una lista guarda coincidencias exactas, nada más difuso. Las regex permisivas son peligrosas aquí: un
*.example.comdescuidado también aceptaattacker-example.com. - Añade
Vary: Origin: sin él, una caché compartida servirá tan contenta la respuesta del dominio A a una petición del dominio B. - Mantén un max-age de preflight razonable:
86400segundos (un día) es un valor típico; mucho más y los cambios de política tardan en propagarse, mucho menos y desperdicias peticiones de preflight. - Restringe métodos y encabezados permitidos: declara solo lo que la API soporta de verdad, porque una política demasiado permisiva solo agranda la superficie de ataque.
Errores Comunes
- Poner
Access-Control-Allow-Origin: *y preguntarse por qué las cookies no viajan cross-origin. - Reflejar el encabezado
Originsin validar, lo que deja la API abierta a cualquier sitio web. - Olvidar responder al preflight
OPTIONS, lo que produce errores CORS en peticiones PUT y DELETE. - No enviar
Vary: Origin, permitiendo que un CDN cachee la respuesta de un origen y la sirva a otro. - Activar
allowCredentialsen una API pública sin validar orígenes, lo que expone tus endpoints autenticados a cualquier sitio malicioso. Consulta el Checklist de Seguridad de APIs para la validación de orígenes.
Solución de Problemas
- El preflight devuelve 204 pero el navegador sigue bloqueando: un 204 pelado suspende el preflight si la respuesta OPTIONS no lleva también los encabezados
Access-Control-Allow-*. Access-Control-Allow-Originno coincide: un origen esscheme + host + port, así quehttps://www.app.comyhttps://app.comson orígenes distintos, igual quehttpyhttps. Registra el encabezadoOriginentrante y compáralo byte a byte con tu lista.- El CDN sirve la respuesta de otro origen: sin
Vary: Origin, una caché compartida puede servir a un origen B la respuesta cacheada del origen A. Si validas orígenes dinámicamente, pon ademásCache-Control: no-storeen las respuestas de preflight. - Las subidas de archivos quedan bloqueadas: un
multipart/form-datadispara preflight, así que falla salvo quePOSTesté enAccess-Control-Allow-MethodsyContent-TypeenAccess-Control-Allow-Headers. Para subidas con URLs prefirmadas de S3, el bucket necesita su propia regla CORS, porque la petición ni siquiera llega a tu servidor. - Fallan las conexiones SSE o WebSocket:
EventSourceno puede mandar encabezados personalizados, así que autentica con cookies máswithCredentialso con un token en el query string. Los WebSockets se saltan CORS por completo; ahí valida el encabezadoOrigindurante el handshake. - El intercambio del token OAuth queda bloqueado: las redirecciones no pasan por CORS, pero el
fetchque cambia el código de autorización por un token sí. El endpoint de tokens del proveedor tiene que devolverAccess-Control-Allow-Originpara tu dominio (Google y GitHub permiten todos los orígenes; un proveedor propio hay que configurarlo).
Ver También
- MDN: Cross-Origin Resource Sharing: la guía de referencia sobre peticiones simples y preflight.
- MDN: Access-Control-Allow-Origin: la semántica exacta del encabezado, incluido por qué
*rompe con credenciales. - Middleware cors de expressjs: el paquete estándar de Node, con callbacks de origen y soporte de credenciales.
- Documentación de CORS en Spring: CORS global y por controlador para Spring Boot.
Preguntas frecuentes
¿Por qué mi API funciona en Postman pero falla en el navegador?
Postman se salta la Same-Origin Policy por completo porque no es un navegador. El navegador descarta la respuesta de una petición cross-origin si el servidor no manda los encabezados Access-Control-Allow-* correctos. En lugar de adivinar, reproduce las comprobaciones del navegador con curl y un encabezado Origin explícito.
¿Puedo usar un comodín para subdominios como *.example.com?
El encabezado no admite patrones: Access-Control-Allow-Origin quiere la cadena exacta del origen. Lo que sí puedes hacer es validar en vivo: comprobar si el origen de la petición termina en .example.com y reflejar el valor exacto de vuelta. Spring Boot lo expone como allowedOriginPatterns; en Express o Flask escribes la comprobación tú mismo. Ojo con el bypass del punto final: evil-example.com debería fallar una validación bien hecha.
¿Necesito CORS si el frontend y la API comparten dominio?
No. CORS solo importa cuando difiere el origen (scheme + host + port). Viviendo ambos en https://example.com, no hace falta configurar nada. Enrutar /api a través de un reverse proxy es la configuración habitual de mismo origen cuando el backend está en otro sitio.
¿Cómo depuro un error de CORS?
Abre DevTools y mira la petición fallida en la pestaña Network; compara el encabezado de respuesta Access-Control-Allow-Origin con el Origin que envió el navegador. Si el fallo es de preflight, inspecciona la respuesta del OPTIONS. No necesitas un navegador para comprobar ninguno de los dos casos: curl -H "Origin: https://yourapp.com" -X OPTIONS https://api.example.com/endpoint hace el trabajo.
¿Cómo permito cookies en peticiones cross-origin?
Activa Access-Control-Allow-Credentials: true en el servidor y credentials: 'include' (fetch) o withCredentials: true (axios) en el cliente. El allow-origin tiene que ser entonces un origen exacto, porque * se rechaza. Las cookies además necesitan su propio SameSite=None; Secure, o el navegador las descarta cross-site igualmente.
¿Cómo configuro CORS en funciones serverless?
Devuelve los encabezados CORS desde la respuesta de la propia función y contesta al OPTIONS con un 204 más los encabezados permitidos. Con API Gateway + Lambda puedes configurarlo a nivel de gateway. Lo configures al nivel que lo configures, mira la respuesta en vivo y confirma que los encabezados están ahí.
¿Los WebSockets usan CORS?
No. Los handshakes de WebSocket quedan fuera de la Same-Origin Policy, así que la vigilancia te toca a ti: el servidor debe revisar Origin durante el upgrade y descartar lo inesperado. CORS tampoco sustituye a la autenticación aquí: mete un token en la URL o en el subprotocolo.
¿Cómo manejo CORS detrás de un API gateway en microservicios?
Configúralo una sola vez en el gateway (Kong, Envoy y AWS API Gateway gestionan el preflight y estampan los encabezados) en lugar de en cada servicio. Un servicio al que se llega sin pasar por el gateway sigue necesitando su propio manejo de CORS. Las llamadas internas entre servicios no pasan por un navegador, así que CORS ni siquiera aplica ahí.
¿Cómo gestiono los orígenes permitidos por entorno?
Guarda la lista en una variable de entorno como CORS_ORIGINS=https://yourapp.com,https://www.yourapp.com, léela al arrancar y valida cada Origin entrante contra ella. Hardcodear orígenes en el código hace que promocionar entre entornos sea propenso a errores.
¿Puedo verificar mi configuración CORS sin navegador?
Sí. Una petición simple es solo curl -H "Origin: https://example.com" -I https://api.example.com/endpoint; añade -X OPTIONS -H "Access-Control-Request-Method: PUT" para sondear el preflight. Si quieres cobertura end-to-end, un test de Playwright puede lanzar peticiones cross-origin desde un contexto de navegador real.
Recursos Relacionados
Llamar a una API REST: Python, JS, Java y Go
Cómo hacer peticiones HTTP a una API REST y manejar la respuesta JSON en Python, JavaScript, Java y Go.
RecipeVersionado de APIs: URL, Header y Media Type
Cómo versionar APIs REST con estrategias de ruta URL, header y media-type manteniendo compatibilidad hacia atrás para los clientes existentes.
RecipeManejar Errores en APIs con RFC 7807
Patrones para un manejo de errores de API consistente y predecible en varios lenguajes y frameworks.
RecipeLimitación de Tasa (Rate Limiting)
Cómo implementar rate limiting en APIs usando token bucket, sliding window y fixed window en Python, JavaScript y Java.
RecipeValidación de Input: Schemas en 3 Lenguajes
Valida el input de usuarios en el límite de la API con librerías de schemas. Ejemplos con Pydantic, Zod y Jakarta Validation, sanitización y errores estructurados.
RecipeOpenAPI 3.1: docs con Swagger UI y Redoc
Guía práctica para documentar APIs REST con OpenAPI. Genera docs interactivas con Swagger UI y Redoc en Python, JavaScript y Java con linting en CI.