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
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
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
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
Originvalue without an allow-list. - Forgetting
OPTIONSpreflight 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
Originvalue. - Allow only known origins from an allow-list.
- Show one allowed origin and one blocked origin.
- Include the headers needed for an
OPTIONSpreflight. - Add a short note explaining why CORS is not authentication.
Check your work
The answer should avoid reflecting arbitrary origins.
Show solution
<?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.