Frontend engineer path · The network and the user · Topic 12 of 17

Browser security: origins, CORS, CSP, cookies

Not started

The browser runs code from many sites side by side, so it keeps them apart. The same-origin policy stops a script on one origin from reading another origin's data. CORS is how a server opts in to sharing across that line. A Content Security Policy limits which scripts may run on your page, which is your second wall against cross-site scripting. Cookie attributes decide when a session travels with a request.

Why it matters: The browser enforces most of a web app's security boundaries for you, but only if you understand them well enough not to switch them off by accident.

  1. 1The origin is the boundary

    Scheme, host and port together. A page can send requests anywhere, but it can only read responses from its own origin unless the other side allows it.

  2. 2CORS relaxes, never tightens

    CORS headers are the server telling the browser which other origins may read its responses. They don't stop requests, and they don't protect your API.

  3. 3XSS is your code running theirs

    If untrusted text reaches the page as HTML or script, it runs with your origin's power. Escape on output, prefer textContent, and add a CSP as a second wall.

  4. 4Cookies carry flags

    HttpOnly hides a cookie from JavaScript, Secure keeps it to HTTPS, and SameSite decides whether other sites' requests carry it.

1

How it works

The explanation, with examples you can run.

Origins

An origin is the scheme, host and port of a URL. https://shop.example and https://shop.example/cart share one; change any of the three parts and it is a different origin:

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

A page may still embed cross-origin images, scripts and styles, and submit forms anywhere. What it can't do by default is read a cross-origin response from fetch, or reach into another origin's frame.

CORS

When a script fetches another origin, the browser sends an Origin header and only hands the response to the script if the reply carries a matching Access-Control-Allow-Origin. Requests that a plain HTML form couldn't make (methods like PUT or DELETE, a JSON Content-Type, custom headers) are preflighted: the browser first sends an OPTIONS request asking permission, and only sends the real one if the answer allows it. With cookies (credentials: "include"), the server must name the exact origin and send Access-Control-Allow-Credentials: true.

CORS is enforced by browsers, for browsers. curl ignores it, so it is never a substitute for authentication on your API.

XSS and CSP

Cross-site scripting is attacker-controlled text ending up in your page as code. Stored XSS comes from your database, reflected XSS from the URL, DOM-based XSS from your own client code (innerHTML, document.write, eval, javascript: URLs). Defences, in order:

  1. Treat data as text. Use textContent, setAttribute and your framework's default escaping; reach for innerHTML only with sanitised HTML.

  2. Escape for the context you are writing into: HTML, an attribute, a URL or a script each need different handling.

  3. Send a strict Content Security Policy, for example script-src 'nonce-RANDOM' 'strict-dynamic'; object-src 'none'; base-uri 'none', with a fresh random nonce on each response. Injected scripts without the nonce don't run, and inline event handlers are blocked. frame-ancestors in the same header controls who may frame your page.

For text inside HTML, escaping five characters is enough to keep it text:

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

Cookies

Session cookies should be HttpOnly (unreadable by scripts, so an XSS can't steal them), Secure (HTTPS only) and SameSite=Lax or Strict. SameSite governs cross-site requests, where a site is the registrable domain plus the scheme, so app.shop.example and api.shop.example are the same site but different origins. Some browsers treat a cookie without SameSite as Lax, others don't, so always set it explicitly. SameSite=None is needed for genuinely cross-site uses such as embedded widgets, and must be combined with Secure.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.A page on https://app.example calls fetch("https://api.example/me", { credentials: "include" }). The API replies with Access-Control-Allow-Origin: *. What happens?
  2. 2.Which line is an XSS hole if name comes from the URL?
    title.textContent = name          // Atitle.innerHTML = `Hi ${name}`    // Btitle.setAttribute("data-name", name)  // C
  3. 3.A session cookie is set with SameSite=Lax. Another site has a hidden form that POSTs to your /transfer endpoint. Is the cookie sent?

0 of 3 right so far. Get all three to finish this topic.