HN Debrief

Developers don't understand CORS (2019)

  • Security
  • Programming
  • Web Development
  • Developer Tools

The submitted post uses Zoom’s old localhost vulnerability to argue that developers regularly get CORS wrong. The comments mostly agreed with the title, but they also said the post itself muddies the core point. The sharpest correction was that `Access-Control-Allow-Origin` does not mean only that origin can send requests to your server. In many cases the browser will still send the request. What changes is whether browser JavaScript can read the response, and for non-simple requests whether the browser will send the real request after a preflight. That distinction is the whole game.

Treat CORS as browser behavior you must design around, not a shield around your API. If your app depends on “the browser won’t send that request,” audit methods, content types, cookie behavior, and CSRF defenses now, especially on localhost services and any endpoint that still accepts form-like requests.

Discussion mood

Frustrated and self-aware. Many admitted they still find CORS confusing, but the dominant mood was that the confusion comes less from inscrutable magic than from developers treating CORS as server-side security instead of a browser-enforced relaxation of the Same-Origin Policy, plus a pile of legacy exceptions around forms, cookies, and simple requests.

Key insights

  1. 01

    CORS does not stop all requests

    The important correction is that CORS is not a gate that prevents foreign sites from contacting your server at all. For simple cross-origin requests, the browser can still send the request. What CORS primarily controls is whether page JavaScript can read the response, and for non-simple requests whether the browser will proceed past preflight. That changes how you must threat-model localhost daemons and internal APIs. If a request itself can cause harm, lack of response visibility is not protection.

    Audit endpoints for side effects on receipt, not just response secrecy. If a cross-origin request landing at all is dangerous, CORS headers are the wrong defense.

      Attribution:
    • muvlon #1 #2
    • paulddraper #1
  2. 02

    Form-compatible POST paths stay dangerous

    The ugly edge case is that browsers still allow some cross-origin POSTs without preflight because plain HTML forms always could. `text/plain`, `multipart/form-data`, and `application/x-www-form-urlencoded` can all reach your server, and attackers can sometimes shape those bodies into valid JSON if your backend ignores or weakly validates `Content-Type`. One commenter gave a pentest example using a `text/plain` form to produce valid JSON on the wire. Another described a real bug where a naive substring check on `application/json` was enough to bypass expectations.

    If your API is JSON-only, enforce exact content-type handling in middleware and reject everything else. Do not disable CSRF protections just because the happy path uses JSON.

      Attribution:
    • Sophira #1 #2
    • RagingCactus #1
    • chuckadams #1
  3. 03

    Custom headers can be a CSRF barrier

    A useful security framing was that requiring a non-simple request characteristic can itself be protective, because the browser will not send that request cross-origin without a successful preflight. A commenter called out that even a made-up custom header works for this purpose. If your endpoint requires `I-Promise-To-Not-Be-Malicious: true`, hostile browser JavaScript cannot add it unless your CORS policy explicitly allows it. That sounds silly, but it captures why `Access-Control-Allow-Headers` exists and why method and header design matter.

    For browser-facing APIs that rely on cookies, require a custom header or another preflight-triggering property on state-changing routes. Then make sure your CORS policy does not broadly allow arbitrary origins and headers.

      Attribution:
    • RagingCactus #1
    • xg15 #1
  4. 04

    Developers mistake CORS for the blocker

    A sharp explanation for the recurring confusion is that newer developers encounter CORS only when the browser console says a cross-origin request failed. That trains them to think CORS is the thing breaking their app, so they go hunting for ways to disable it. The better model is the reverse. The Same-Origin Policy is the default restriction. CORS is the protocol for selectively allowing the request pattern you actually want.

    Teach SOP before CORS in onboarding and docs. Teams that start with 'how to fix the browser error' will keep shipping permissive headers they do not understand.

      Attribution:
    • JimDabell #1
    • brazukadev #1
  5. 05

    The tooling makes correct debugging harder

    Several experienced commenters said the browser experience nudges people toward cargo-cult fixes. The actual failing step is often the preflight, but dev tools bury it or present blocked requests in a confusing way. Frontend engineers see a browser error, while the real fix usually lives in backend headers and endpoint behavior. That split encourages random header tweaking instead of reasoning from the request class, preflight, and response headers.

    When debugging, inspect the OPTIONS preflight first and document the expected method, headers, and origin for each route. A short internal runbook will beat another round of Stack Overflow snippets.

      Attribution:
    • deathanatos #1
    • moring #1
    • yen223 #1
    • cyberrock #1
  6. 06

    Legacy form behavior is why CORS feels arbitrary

    The part that makes CORS hard to remember is not just complexity for its own sake. The rules are shaped around backward compatibility with what browsers and HTML forms already allowed long before modern JavaScript APIs existed. That is why the system has so many branches, and why some dangerous-seeming cross-origin requests still work while others need preflight. Once you see that CORS is layered on top of old form semantics and ambient cookies, the odd rule set stops looking random and starts looking fossilized.

    When designing browser APIs, assume old form semantics still matter. If your security story depends on modern fetch behavior alone, you probably missed a legacy path.

      Attribution:
    • preommr #1
    • almog #1
    • somat #1

Against the grain

  1. 01

    Preflight can still be a real security boundary

    Against the louder 'CORS only hides responses' line, a few commenters pushed a more practical view. For endpoints designed around non-simple methods, JSON payloads, or custom headers, the browser will not send the actual cross-origin request without a successful preflight. In that setup CORS is not just cosmetic. It can materially prevent browser-mediated state changes, provided you do not also expose the same action through GET or form-compatible POSTs.

    Do not dismiss CORS outright if your API already uses preflight-triggering methods and headers. Verify that dangerous actions are only reachable through those routes and remove legacy alternate paths.

      Attribution:
    • xg15 #1
    • bazoom42 #1
  2. 02

    Same-origin architecture is not always practical

    One clean fix offered was to serve frontend and backend behind one origin and avoid CORS entirely. Another commenter pushed back that this is architecture-dependent and not always desirable. Separate infrastructure, independent scaling, public APIs, or organizational boundaries can make single-origin routing the wrong tradeoff. The simplification is real, but it is not free.

    Prefer same-origin deployments when the product allows it, but do not force awkward infrastructure just to escape CORS. If you stay split-origin, budget for explicit policy design and testing.

      Attribution:
    • rubendev #1
    • ricardobeat #1
  3. 03

    SOP trivia is weak interview signal

    Some people praised Same-Origin Policy questions as a hiring filter. A pushback worth keeping was that CORS knowledge is often 'set it and forget it' work. Senior people who built and shipped the right thing years ago may still stumble on protocol details in an interview. That does not mean they cannot reason about browser security when it matters.

    Use CORS questions to test debugging approach and threat modeling, not memorized header trivia. Ask candidates how they would trace a failing preflight or lock down a JSON API.

      Attribution:
    • christophilus #1

In plain english

415
HTTP 415 Unsupported Media Type, a response code meaning the server refuses the request body format.
application/x-www-form-urlencoded
The classic HTML form encoding where fields are sent as key=value pairs joined with ampersands.
Content-Type
An HTTP header that declares the media type of the body being sent, such as application/json or text/plain.
CORS
Cross-Origin Resource Sharing, a browser protocol that lets a server say which other website origins may read its responses or make certain cross-origin requests.
GET
An HTTP method intended for fetching data without changing server state.
JSON
JavaScript Object Notation, a widely used text format for structured data exchange.
localhost
The local machine address used for services running on the same computer as the browser.
multipart/form-data
An HTML form encoding used for file uploads and multiple parts in one request body.
Playwright
A browser automation and testing framework that can be configured to bypass normal browser restrictions in controlled environments.
POST
An HTTP method commonly used to submit data or trigger actions on a server.
preflight
A browser-sent OPTIONS request that checks whether a server allows a planned cross-origin request with certain methods or headers before sending the real request.
text/plain
A simple text media type that browsers may allow in form-like cross-origin requests without preflight.

Reference links

Core documentation

Standards and specs

Background references

Tools and implementation notes