CORS Headers for Nginx, Apache, Express and PHP: Practical Comparison
Reviewed September 3, 2026 · Maintained by William
CORS policy should start with the trust decision, not with a copy-pasted server snippet. The examples below express the same intention: only https://app.example.com may make the relevant cross-origin browser request.
Nginx
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Vary "Origin" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
Handle OPTIONS intentionally and test how errors inherit headers; Nginx directive inheritance can surprise deployments.
Apache
Header always set Access-Control-Allow-Origin "https://app.example.com"
Header always set Vary "Origin"
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type"
This requires the relevant header module and still needs correct OPTIONS routing.
Express
const allowed = new Set(['https://app.example.com']);
app.use((req, res, next) => {
const origin = req.get('Origin');
if (origin && allowed.has(origin)) {
res.set('Access-Control-Allow-Origin', origin);
res.set('Vary', 'Origin');
}
next();
});
In real applications, a maintained CORS middleware package is usually preferable to reimplementing the full protocol.
PHP
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$allowed = ['https://app.example.com'];
if (in_array($origin, $allowed, true)) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Vary: Origin');
}
What should stay the same in every stack
- Do not reflect arbitrary origins.
- Do not combine wildcard origin with credentialed requests.
- Keep allowed methods/headers as narrow as practical.
- Authenticate the real request independently of CORS.
- Test successful and error responses, because missing headers on a 401/500 can look like a mysterious browser CORS failure.
Use the CORS Header Generator to reason about a policy, then translate it to the exact server or framework docs for your deployment.
References
Start with one policy before choosing syntax
Configuration examples are safer when the policy is written in plain language first. For example: “Only https://app.example may call GET and POST; authorization and content-type headers are accepted; credentials are allowed.” Nginx, Apache, Express and PHP can all implement that policy, but their syntax and request-routing behavior differ.
Nginx pattern
add_header Access-Control-Allow-Origin "https://app.example" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Vary "Origin" always;
Handle OPTIONS deliberately and be careful with conditional add_header rules. Test both successful and error responses because header inheritance can surprise deployments.
Apache pattern
Header always set Access-Control-Allow-Origin "https://app.example"
Header always set Access-Control-Allow-Credentials "true"
Header merge Vary "Origin"
This requires the appropriate headers module and should be scoped to the intended virtual host/location rather than copied globally without review.
Express pattern
import cors from 'cors';
app.use(cors({
origin: 'https://app.example',
credentials: true,
methods: ['GET', 'POST']
}));
Middleware placement matters: make sure preflight requests reach the CORS middleware before a route/auth layer rejects OPTIONS.
PHP response pattern
header('Access-Control-Allow-Origin: https://app.example');
header('Access-Control-Allow-Credentials: true');
header('Vary: Origin');
Application-level headers are useful when policy depends on application data, but a reverse proxy can still add, remove or duplicate headers. Inspect the final network response.
Avoid duplicate CORS layers
One of the harder production bugs occurs when the CDN, web server and application all add CORS headers. Browsers can reject malformed or duplicated values even though each layer looks reasonable in isolation. Decide which layer owns the policy, then remove conflicting rules from the others.
Comparison checklist
| Layer | Strength | Risk to watch |
|---|---|---|
| Nginx/Apache | Efficient and central for many routes | Inheritance, location matching, error responses |
| Framework middleware | Easy to tie to application environments | Middleware order, duplicated proxy headers |
| Per-script PHP | Explicit for a small endpoint | Repeated logic and inconsistent endpoints |
Deployment test
After changing configuration, test from an actual browser origin, not just from the same site and not only with cURL. Capture OPTIONS and final responses. If credentials are involved, test an authenticated request and an untrusted origin. A configuration that works for the allowed origin should also fail safely for an origin you did not approve.