web php

CORS

CORS, or Cross-Origin Resource Sharing, is a browser security mechanism. It controls when JavaScript running on one origin is allowed to read responses from another origin.

CORS is not authentication. It does not stop non-browser clients from calling your API. It tells browsers which front-end origins are allowed to read a cross-origin response.

Origins

An origin is scheme, host, and port together:

https://app.example.com
https://api.example.com
http://localhost:5173

Those are different origins. If JavaScript from https://app.example.com calls https://api.example.com, the browser checks CORS headers on the API response.

Allow known origins

Do not blindly reflect every Origin header. Use an allow-list.

PHP example
<?php

declare(strict_types=1);

function allowedCorsOrigin(string $origin, array $allowedOrigins): ?string
{
    return in_array($origin, $allowedOrigins, true) ? $origin : null;
}

$allowed = ['https://app.example.com', 'http://localhost:5173'];

echo allowedCorsOrigin('https://app.example.com', $allowed) ?? 'blocked';
echo PHP_EOL;
echo allowedCorsOrigin('https://evil.example', $allowed) ?? 'blocked';
echo PHP_EOL;

// Prints:
// https://app.example.com
// blocked

If credentials such as cookies are involved, you cannot combine Access-Control-Allow-Credentials: true with Access-Control-Allow-Origin: *.

Preflight requests

For some cross-origin requests, the browser sends an OPTIONS preflight before the real request. The API must answer which methods and headers are allowed.

PHP example
<?php

declare(strict_types=1);

http_response_code(204);
header('Access-Control-Allow-Origin: https://app.example.com');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

// Output body is intentionally empty for a 204 response.

Handle preflight before normal route logic where appropriate. Otherwise the browser may block the actual request even though the endpoint works from curl or Postman.

Credentials Change The Rules

If a browser request sends cookies or HTTP authentication, the server must opt into credentialed CORS deliberately:

PHP example
<?php

declare(strict_types=1);

header('Access-Control-Allow-Origin: https://app.example.com');
header('Access-Control-Allow-Credentials: true');
header('Vary: Origin');

echo 'Credentialed CORS headers prepared.' . PHP_EOL;

// Prints:
// Credentialed CORS headers prepared.

Access-Control-Allow-Origin must name the allowed origin. It cannot be * when credentials are allowed.

Vary: Origin matters when a cache may store the response. It tells caches that the response can change based on the request origin.

Keep CORS At One Boundary

Configure CORS centrally in middleware, the framework, or the edge layer. If individual controllers invent their own headers, error responses and preflight requests are easily missed.

Test both allowed and rejected origins, including error responses. A route that only sends CORS headers on successful requests can be difficult for front-end developers to debug.

Common mistakes

  • Thinking CORS protects an API from all clients.
  • Returning * while also trying to use cookies.
  • Reflecting any Origin value without an allow-list.
  • Forgetting OPTIONS preflight handling.
  • Adding CORS headers to only success responses but not errors.

What you should be able to do

After this lesson, you should be able to explain origins, identify when CORS applies, allow specific front-end origins, understand preflight and credentialed requests, set Vary: Origin when needed, and avoid treating CORS as authentication.

Practice

Task: Allow Known CORS Origins

Create a PHP helper or checklist for a JSON API used by a separate browser front-end.

Requirements

  • Accept an Origin value.
  • Allow only known origins from an allow-list.
  • Show one allowed origin and one blocked origin.
  • Include the headers needed for an OPTIONS preflight.
  • Add a short note explaining why CORS is not authentication.

Check your work

The answer should avoid reflecting arbitrary origins.

Show solution
PHP example
<?php

declare(strict_types=1);

function corsOriginFor(string $origin, array $allowedOrigins): ?string
{
    return in_array($origin, $allowedOrigins, true) ? $origin : null;
}

$allowed = ['https://app.example.com', 'http://localhost:5173'];

foreach (['https://app.example.com', 'https://evil.example'] as $origin) {
    echo $origin . ' -> ' . (corsOriginFor($origin, $allowed) ?? 'blocked') . PHP_EOL;
}

// Prints:
// https://app.example.com -> https://app.example.com
// https://evil.example -> blocked

For an allowed preflight response, send headers like:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization

CORS is not authentication because it is enforced by browsers. Other clients can still call the API, so the server must still check credentials, permissions, validation, and rate limits.