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

LayerStrengthRisk to watch
Nginx/ApacheEfficient and central for many routesInheritance, location matching, error responses
Framework middlewareEasy to tie to application environmentsMiddleware order, duplicated proxy headers
Per-script PHPExplicit for a small endpointRepeated 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.

About the review

This guide is maintained by William. Technical claims are checked against primary or authoritative references where applicable. See How We Test CodeNimbleTools for the site-wide review and correction process.