Browser security boundaries

Understand the origin boundary

Use scheme, host, and port to reason about which documents and requests can share browser authority.

The browser draws its main security line around the origin. An origin is the triple of scheme, host, and port. All three must match, or the browser treats two pages as strangers.

So https://app.flaviocopes.com and http://app.flaviocopes.com are different origins. The scheme differs. https://app.flaviocopes.com and https://app.flaviocopes.com:8443 are different too, because of the port. The host is compared character by character, so flaviocopes.com and www.flaviocopes.com don’t match either.

I like to write the three parts out when I’m unsure. It takes ten seconds and removes a lot of guessing.

Sending is not the same as reading

The same-origin policy stops one origin from reading another origin’s data. It does not stop the request from being sent. Most confusion about browser security starts right here.

Here is a script running on a hostile page. Watch what happens to the request and to the response:

// Runs on https://evil.example
// The request IS sent, and cookies for the target may ride along
fetch('https://api.flaviocopes.com/account', { credentials: 'include' })
  .then(res => res.json()) // ← the browser blocks reading this response
  .catch(err => console.log('read blocked:', err))

The fetch() leaves the browser and reaches your server. Your server runs the handler and sends a response. The browser then refuses to hand that response to the script, and you see something like read blocked: TypeError: Failed to fetch in the console. Your server log, though, shows a real GET /account with the session cookie attached.

An HTML form is even more relaxed. It can POST to any origin, and the request arrives at your server like any other:

<!-- A plain form posts cross-origin with no restriction -->
<form action="https://api.flaviocopes.com/account/email" method="post">
  <input name="email" value="[email protected]">
</form>

Notice that https://app.flaviocopes.com and https://api.flaviocopes.com belong to the same company but not to the same origin. The browser will happily send that form from one to the other. It only refuses to let JavaScript read the reply.

Why this matters for your design

When someone says “the browser blocks cross-origin requests”, they are hiding this difference. The browser blocks reading. Sending, cookies, navigation, and embedding each follow their own rules, and you need to think about each one separately. The next lessons cover them one at a time.

Try this on your own project: pick five URLs you use and write down scheme, host, and port for each. Then open the console on a page from a different origin, run one fetch() and submit one form against your API. For each, note whether the request reached your server logs and whether the page could read the response. The two answers are usually different.

Lesson completed