CORS (Cross-Origin Resource Sharing) es el mecanismo que controla qué dominios externos pueden hacer peticiones a tu API. Cuando una página web en https://miapp.com hace un fetch() a https://api.miapp.com, el navegador primero comprueba si la API permite esa petición. Sin las cabeceras CORS correctas, el navegador bloquea la respuesta aunque haya llegado correctamente al servidor.
Las cabeceras básicas
<?php
// Permitir cualquier origen solo para APIs públicas sin autenticación
header('Access-Control-Allow-Origin: *');
// Permitir un origen específico
header('Access-Control-Allow-Origin: https://miapp.com');
// Métodos permitidos
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
// Cabeceras que el cliente puede enviar
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
// Tiempo que el navegador cachea el resultado del preflight (en segundos)
header('Access-Control-Max-Age: 86400');
Peticiones preflight (OPTIONS)
Antes de ejecutar ciertas peticiones «no simples» (con método PUT/DELETE o cabeceras personalizadas), el navegador envía automáticamente una petición OPTIONS para preguntar qué está permitido. Tu servidor debe responder a ella con las cabeceras CORS y un código 204:
<?php
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header('Access-Control-Allow-Origin: https://miapp.com');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Max-Age: 86400');
http_response_code(204);
exit;
}
Whitelist de orígenes
Cuando tienes varios dominios permitidos (frontend en varios entornos, aplicación móvil, etc.) no puedes poner varios Access-Control-Allow-Origin; solo se admite uno. La solución es validar el Origin entrante contra una lista y reflejarlo si está en la lista:
<?php
$origenesPermitidos = [
'https://miapp.com',
'https://www.miapp.com',
'https://staging.miapp.com',
'http://localhost:3000', // solo en desarrollo
];
$origenSolicitado = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origenSolicitado, $origenesPermitidos, true)) {
header("Access-Control-Allow-Origin: $origenSolicitado");
header('Vary: Origin'); // Importante para que las cachés no mezclen respuestas
} else {
http_response_code(403);
exit;
}
Access-Control-Allow-Credentials
Si tu API necesita cookies o cabeceras de autenticación en peticiones cross-origin, el cliente debe enviar credentials: 'include' y el servidor debe responder con Access-Control-Allow-Credentials: true. En ese caso no puedes usar * en Allow-Origin; debes especificar el origen exacto:
<?php
// Con credenciales: origen debe ser exacto, no wildcard
header('Access-Control-Allow-Origin: https://miapp.com');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
Middleware reutilizable
<?php
class CorsMiddleware {
private array $origenesPermitidos;
private array $metodosPermitidos;
private array $cabecerasPermitidas;
private bool $conCredenciales;
public function __construct(
array $origenes = ['*'],
array $metodos = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
array $cabeceras = ['Content-Type', 'Authorization'],
bool $credenciales = false
) {
$this->origenesPermitidos = $origenes;
$this->metodosPermitidos = $metodos;
$this->cabecerasPermitidas = $cabeceras;
$this->conCredenciales = $credenciales;
}
public function handle(): void {
$origen = $_SERVER['HTTP_ORIGIN'] ?? '';
if ($this->origenesPermitidos === ['*']) {
header('Access-Control-Allow-Origin: *');
} elseif (in_array($origen, $this->origenesPermitidos, true)) {
header("Access-Control-Allow-Origin: $origen");
header('Vary: Origin');
}
header('Access-Control-Allow-Methods: ' . implode(', ', $this->metodosPermitidos));
header('Access-Control-Allow-Headers: ' . implode(', ', $this->cabecerasPermitidas));
header('Access-Control-Max-Age: 86400');
if ($this->conCredenciales) {
header('Access-Control-Allow-Credentials: true');
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
}
}
// Uso en tu punto de entrada (index.php)
$cors = new CorsMiddleware(
origenes: ['https://miapp.com', 'http://localhost:3000'],
credenciales: true
);
$cors->handle();
Errores comunes
- Las cabeceras CORS deben enviarse antes de cualquier output. Si ya has impreso algo (o hay un BOM en el fichero), las cabeceras llegarán tarde y CORS fallará.
- No confundas servidor y navegador. CORS lo aplica el navegador, no el servidor. Una herramienta como
curlo Postman ignora CORS porque no es un navegador. - No son seguridad total. CORS controla qué otros dominios pueden hacer peticiones desde el navegador; no sustituye a la autenticación ni a la autorización del servidor.
- Cloudflare y proxies. Si tu app está detrás de un CDN o proxy, comprueba que no añade sus propias cabeceras CORS que puedan entrar en conflicto con las tuyas.
