CORS en PHP: cabeceras Access-Control-*, peticiones preflight y configuración segura

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 curl o 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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP