The same-origin policy (SOP) is the most important security boundary in the browser. It stops a script on one website from reading data that belongs to another, which is why a blog in one tab cannot read your bank balance from another. CORS (Cross-Origin Resource Sharing) is the controlled way to relax that boundary when you want it relaxed.
In this lesson you will learn what an origin is, exactly what SOP blocks, how the CORS handshake works, how to configure it safely in Express, and the misconfigurations that turn CORS into a vulnerability.
An origin is the combination of scheme, host and port. Two URLs share an origin only if all three match.
| URL | Same origin as https://shop.example.com/cart? | Why |
|---|---|---|
| https://shop.example.com/checkout | Yes | Only the path differs |
| http://shop.example.com/cart | No | Different scheme |
| https://api.shop.example.com/ | No | Different host (subdomain) |
| https://shop.example.com:8443/ | No | Different port |
SOP is a read restriction, not a send restriction. A page on evil.example.com can still:
fetch() request to any origin.What it cannot do is read the response of a cross-origin fetch() or the DOM of a cross-origin iframe. The request still reaches your server; only the answer is hidden. That is why SOP alone does not prevent CSRF: the attacker does not need to read the reply to cause damage.
CORS lets a server declare which foreign origins may read its responses. For "simple" requests (GET, HEAD, POST with form content types and no custom headers) the browser sends the request with an Origin header and checks the answer:
GET /api/profile HTTP/1.1
Origin: https://app.example.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: OriginIf the Access-Control-Allow-Origin value does not match, the browser discards the response and the script sees a network error. Anything more complex (JSON bodies, Authorization headers, PUT or DELETE) triggers a preflight: an OPTIONS request that asks permission before the real request is sent. The server answers with Access-Control-Allow-Methods, Access-Control-Allow-Headers and Access-Control-Max-Age.
The sample code above shows a safe configuration using the cors package. The rules it encodes:
Origin header arrives.credentials: true is set, the browser rejects a wildcard *, so an explicit origin is mandatory.Vary: Origin (the package does this) so caches do not serve one origin's headers to another.methods and allowedHeaders to what the frontend actually uses.On the client, cookies are only attached to cross-origin requests when you ask for them:
const res = await fetch("https://api.example.com/api/profile", {
credentials: "include" // send cookies; server must allow this origin explicitly
});
console.log(await res.json()); // { name: "Ada" }null. Sandboxed iframes and local files send Origin: null; allowing it means allowing everyone.curl ignores CORS completely, so authentication and authorization must still happen on the server./example\.com$/ also matches notexample.com. Anchor the whole origin:const ok = /^https:\/\/([a-z0-9-]+\.)?example\.com$/.test(origin);Which pair of URLs shares the same origin?
Access-Control-Allow-Origin and related headers; complex requests are preceded by an OPTIONS preflight.* with credentials.Next lesson: Cookies and Session Management: HttpOnly, Secure and SameSite — the cookie attributes that keep sessions from being stolen or forged.