skip to content

When a request comes from an origin missing from Laravel's allowed_origins, does HandleCors stop it before the controller runs?

level: middleimportance: should knowfreq 38%

answer

  1. headers only, never a 403
  2. actual request: $next first, headers after
  3. preflight answered by HandleCors itself
  4. the browser withholds the response
  5. CSRF and auth guard the state

basics

~20 s

No. For an actual request HandleCors always passes it on, so routing, middleware and the controller run. HandleCors only decides which Access-Control headers go on the response, and the browser then withholds that response from the calling page.

solid answer

~40 s

`HandleCors` is not an access-control layer. For a normal request on a covered path it calls `$next($request)` first and adds `Access-Control-*` headers afterwards, so the route, its middleware and the controller run whatever the `Origin` is. It has no branch that returns a 403. A disallowed origin just gets headers that do not grant it: with one configured origin the header carries that configured origin, and with wildcard entries that do not match it is absent. The browser then hides the body from the page. Preflights are different: `HandleCors` answers them itself, so a call that needs one never gets sent from a disallowed origin. Protecting state from foreign sites — say, a form-encoded POST that changes something — is the job of CSRF protection and authentication, not `config/cors.php`. Non-browser clients ignore CORS entirely.

code

bash · 3 lines
bash
curl -i -X POST https://api.tallyroom.example/api/newsletter \
  -H 'Origin: https://evil.example' \
  -d '[email protected]'

go deeper

for a junior

Remember that HandleCors only adds response headers and the browser enforces the result; the controller still runs.

for a middle

Walk through the preflight and actual-request branches of HandleCors, and what a mismatched origin receives in each case.

for a senior

Name the real controls for a public endpoint — CSRF, auth, rate limiting, validation — and read CORS failures from headers rather than status codes.

for a principal

Frame CORS as protecting users rather than servers when teams propose using it to restrict API access, and redirect them to authentication.

## What HandleCors does with a request `Illuminate\Http\Middleware\HandleCors` sits in Laravel's global middleware stack. For a request whose path matches `paths` in `config/cors.php`, its `handle()` method follows one of two branches: 1. **Preflight** — an `OPTIONS` request carrying `Access-Control-Request-Method`. `HandleCors` builds the answer itself (a `204` in the framework's tests) and returns it. Routing, route middleware and the controller never run. 2. **Actual request** — anything else. `HandleCors` calls `$next($request)`, so the rest of the global stack, the route's middleware and the controller all execute. Only when the response comes back does it add the `Access-Control-*` headers the configuration allows. Neither branch has a rejection path. The middleware never returns a 403, and it never throws an exception for a foreign origin. CORS enforcement happens in the **browser**, which compares the response headers with the page's origin and decides whether script may read the response. ## What a mismatched origin receives The framework's own `HandleCorsTest` pins down what the preflight answer carries: | `allowed_origins` | Request `Origin` | `Access-Control-Allow-Origin` sent | |---|---|---| | `['http://localhost']` | `http://localhost` | `http://localhost` | | `['http://localhost']` | `http://otherhost` | `http://localhost` — the browser sees the mismatch | | `['*']`, credentials off | any | `*` | | `['*.laravel.com']` | `http://test.laravel.com` | `http://test.laravel.com` | | `['*.laravel.com']` | `http://api.service.test.laravel.com` | echoed as well: the wildcard spans nested subdomains | | `['*.laravel.com']` | `http://test.example.org` | no header at all | Two consequences follow. With a single configured origin, the header is **always present**, so seeing it in a response proves nothing about the caller. And a wildcard entry like `*.laravel.com` matched an `http://` origin in the test: the asterisk swallows the scheme too, so it admits plain-http subdomains. `allowed_origins_patterns` holds pattern entries for the cases where a single wildcard is too blunt. ## Why the controller still runs - **Simple requests are not preflighted.** A form-encoded `POST` or a plain `GET` from another origin goes straight to the server. The browser checks CORS only on the response. - **Side effects happen anyway.** A foreign page can make the newsletter endpoint insert a row, even though it can never read the reply. - **Server logs look normal.** The access log shows a `200`, while the calling page reports a network error. - **Non-browser clients ignore CORS.** A script or server-to-server client never reads `Access-Control-*` headers at all. ## What actually protects the endpoint For the marketing site calling a SaaS API, the protections sit elsewhere in Laravel: - **Request-forgery protection** in the `web` middleware group rejects state-changing session requests that lack a valid token. - **Authentication and authorisation** (`auth:sanctum`, policies) decide who may act, whatever the origin. - **Rate limiting and validation** keep a public endpoint such as a newsletter signup from being abused, whether or not a browser is involved. CORS protects the **user**: a malicious page cannot use the visitor's browser to read data the API returns. It does not protect the **server** from receiving requests. ## Practical checks 1. To test a foreign origin, send the request with an `Origin` header and read the response headers, not the status code. 2. Do not add origin checks in `HandleCors`'s place to "harden" CORS. If an endpoint must refuse foreign callers, give it authentication or a signed token. 3. Keep `allowed_origins` narrow anyway. It still decides which pages may **read** responses, and that matters most once credentials are involved. 4. When a front-end developer reports "the API rejected my origin", check the server logs for the request before changing anything: if the controller ran, the server did its part and the fix is in the headers. ## Why interviewers ask this The question separates candidates who treat `config/cors.php` as a firewall from those who know where enforcement happens. A candidate who says "we locked CORS down, so only our site can call the API" has left the API open to every non-browser client and to simple cross-origin posts. The strong answer places each control at its layer: CORS for what a page may read, request-forgery protection for cookie-bearing writes, and authentication for who may act at all.

  • If HandleCors never rejects requests, what stops a disallowed origin's JSON PUT from reaching a Laravel controller?
    The browser. A JSON `PUT` is not a simple request, so the browser sends a preflight first. `HandleCors` answers that preflight without granting the foreign origin, and the browser never sends the `PUT`. A non-browser client skips the preflight and reaches the controller, so authentication still has to refuse it.
  • Can a Laravel app log which foreign origins are calling it?
    Yes, through a middleware of your own that reads `$request->header('Origin')` and records values missing from `config('cors.allowed_origins')`. `HandleCors` itself does not log or reject anything. The log only shows what browsers attempted; it is not a security control.

HandleCors works like a receptionist who processes every form handed in, then stamps the receipt with who may see it. The browser is the courier, and it refuses to hand an unstamped receipt back to the page. The work was still done.

saying these in an interview costs you the question

  • HandleCors returns a 403 for origins missing from allowed_origins.
  • A disallowed origin's simple POST never reaches the controller.
  • An Access-Control-Allow-Origin header in the response proves the caller was allowed.
  • Tight CORS settings protect the API from curl and server-side scripts.
  • CORS replaces CSRF protection for cross-origin form posts.