The Web Security Model

Imagine your browser has fifty tabs open: your bank, your webmail, a news site, and a page you clicked from an ad. Every one of those pages can run JavaScript. That JavaScript executes with real privileges. It can read cookies, make network requests, and manipulate what you see. Without some hard rule separating what one site's code can touch from another's, opening the wrong tab could let a malicious page steal your bank session or read your private email. The entire safety of the modern web rests on a single boundary. That boundary makes this impossible by default. That boundary is the same-origin policy (SOP), enforced by every major browser. It is the foundational trust mechanism of the web platform. Layered on top of it is the machinery that lets a stateless protocol — HTTP — remember who you are across requests: cookies and sessions. Together, SOP and session management define a trust model. In this model, the browser acts as a mutually suspicious referee. It referees between multiple, mutually distrusting parties: the user, the visited site, and any other site whose script or image happens to load in the same page. This article builds that model from the ground up. We start with why HTTP's statelessness forces servers to invent identity persistence. We then show exactly how cookies implement it and what attributes control their exposure. We then define the same-origin policy precisely — what counts as an "origin," what it restricts, and what it deliberately does not restrict. That last gap is the seed of many later vulnerabilities. Finally we look at the trust boundaries the model creates between browser, server, and third-party content. Almost every browser-side web vulnerability is best understood as a violation, gap, or misuse of this exact boundary. Understanding this model is a prerequisite for understanding almost all client-side web attacks. Cross-Site Scripting is fundamentally a same-origin-policy bypass. It is achieved by tricking a page into running attacker script as if it were the page's own origin. Cross-Site Request Forgery exploits the fact that cookies are attached to requests automatically. This happens regardless of who triggered them. SQL Injection lives on the server side of this boundary. It is reachable precisely because the browser faithfully delivers attacker-controlled input across the trust line the SOP does not police. Mastering this article means the rest of web security stops looking like a list of tricks. Instead, it starts looking like consequences of one boundary being probed for holes.

Key takeaways
  • HTTP is stateless; cookies let servers attach persistent identity (a session) to a sequence of otherwise-unrelated requests.
  • The same-origin policy restricts scripts to read data only from pages sharing scheme, host, and port with their own origin.
  • SOP does not stop a page from sending requests to other origins — it stops it from reading the response, which is a critical asymmetry.
  • Cookie attributes (`HttpOnly`, `Secure`, `SameSite`, `Domain`, `Path`) are the practical knobs that narrow a cookie's exposure.
  • Sessions turn a leaked cookie into full account takeover, making cookie confidentiality the crux of browser-server trust.
  • Nearly every client-side web vulnerability is a gap, exception, or misuse inside this exact model — SOP violations, cookie leakage, or state confusion.
The core insight

The web's entire client-side security rests on one rule — a script may only read data from its own origin — and almost every browser-based attack is a way of getting around, weakening, or exploiting an exception to that rule.

The idea

The web security model exists because two design decisions, made decades apart, created a gap that had to be filled with policy. First, HTTP is stateless: each request is independent, carrying no memory of prior requests. Second, browsers began executing active content (JavaScript) from arbitrary sites, and users routinely have many sites' content loaded simultaneously. Statelessness demanded a mechanism to fake memory — the cookie and the session. Active content from many sources demanded a mechanism to keep sites from spying on or manipulating each other — the same-origin policy. These two mechanisms, and the exact boundary between them, constitute "the web security model." An origin is defined as the triple `(scheme, host, port)` — for example `https://bank.example:443`. Two URLs are same-origin only if all three match exactly; `http://bank.example` and `https://bank.example` are different origins despite sharing a hostname, and `https://app.bank.example` differs from `https://bank.example` despite sharing a parent domain. The same-origin policy says: a script running in the context of origin A may freely read the DOM, cookies, and local storage of documents from origin A, but may not read the contents of a document, response body, or storage belonging to origin B, unless B explicitly opts in (via mechanisms like Cross-Origin Resource Sharing-style headers). Critically, SOP restricts reading, not always sending — a page can still cause a cross-origin `GET`/`POST` to fire (an `` tag, a `

` submit, a `fetch` with default mode), it simply cannot inspect the reply. This asymmetry is the root cause of CSRF. A session is server-side state (e.g., "user 4471 is logged in with role admin") that must be re-associated with each incoming HTTP request. The near-universal mechanism is: the server generates a large random session identifier, stores it in a cookie, and the browser re-attaches that cookie to every subsequent request to the matching origin/domain automatically. The server looks up the session ID in its own store to recover the state. This means a cookie is, functionally, a bearer credential: whoever presents it is the logged-in user, as far as the server can tell — which is why cookie confidentiality and the SOP that protects it are inseparable concerns. Understanding this requires the prior concept of Authentication and Password Security: a password authenticates you once at login; the cookie is what carries that authentication forward through a session of stateless requests.

How it works

The full round trip involves four cooperating pieces: the login exchange that establishes a session, the cookie jar that persists it, the browser's per-request cookie-attachment logic, and the SOP enforcement that gates script-level access to cross-origin data. We walk through each as a mechanism, then give pseudocode for the parts a security engineer actually configures: cookie issuance and the origin-comparison check.

1. Establishing a session. The user submits credentials over HTTPS. The server verifies them (password hashing/verification, per Authentication and Password Security), creates a session record server-side, and returns a `Set-Cookie` header binding a random session ID to the browser.

2. Persisting it. The browser stores the cookie in its cookie jar, indexed by the cookie's `Domain`/`Path` scope, honoring attributes that restrict when it is sent.

3. Automatic reattachment. On every subsequent request whose URL matches the cookie's scope, the browser attaches `Cookie: sid=...` — without the page's JavaScript needing to do anything, and in fact often without JavaScript being able to see the cookie at all if `HttpOnly` is set.

4. Origin-gated access. When script from origin A tries to read something belonging to origin B (via `XMLHttpRequest`/`fetch` response bodies, `iframe.contentDocument`, cross-window property access, etc.), the browser's SOP check intervenes and blocks the read unless B has opted in. ```pseudocode

# --- Server side: issuing and validating a session cookie --- function LOGIN(username, password): user = LOOKUP_USER(username) if user is null or not VERIFY_PASSWORD(password, user.hash): return HTTP_401 sid = RANDOM_BYTES(256_BITS) # cryptographically secure RNG SESSION_STORE[sid] = { # server-side state, NOT sent to client user_id: user.id, created_at: NOW(), expires_at: NOW() + SESSION_TTL } response = HTTP_200 response.headers["Set-Cookie"] = "sid=" + sid + "; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=" + SESSION_TTL return response function HANDLE_REQUEST(request): sid = request.cookies["sid"] if sid is null: return ANONYMOUS_CONTEXT record = SESSION_STORE.get(sid) if record is null or record.expires_at < NOW(): return ANONYMOUS_CONTEXT # expired or forged/unrecognized id return AUTHENTICATED_CONTEXT(record.user_id)

# --- Browser side: the same-origin check (conceptual) --- function ORIGIN(url): return (url.scheme, url.host, url.port) function SAME_ORIGIN(url_a, url_b): return ORIGIN(url_a) == ORIGIN(url_b) function CAN_SCRIPT_READ(requesting_document, target_resource): if SAME_ORIGIN(requesting_document.url, target_resource.url): return ALLOW if target_resource.has_opt_in_header_for(requesting_document.origin): return ALLOW # explicit cross-origin sharing opt-in return DENY # default: block the READ (send may still have happened) function ATTACH_COOKIES(request): for cookie in COOKIE_JAR: if cookie.domain_matches(request.host) and cookie.path_matches(request.path) and (not cookie.secure or request.scheme == "https") and cookie.samesite_permits(request.is_cross_site, request.method): request.headers["Cookie"] += cookie.name + "=" + cookie.value return request ``` The `SameSite` check deserves a closer look because it is the browser's built-in partial mitigation for CSRF, and it interacts directly with the send/read asymmetry described above:

  • `SameSite=Strict` — cookie is attached only to requests originating from the same site (never on cross-site navigation or subresource loads).
  • `SameSite=Lax` (modern default) — cookie is attached on top-level, "safe" cross-site navigations (e.g., clicking a link that does a `GET`), but not on cross-site `POST`s or subresource requests (images, iframes).
  • `SameSite=None` — cookie attached on all requests including cross-site ones; requires `Secure`.
Figure
Trust boundaries: origins, cookies, and the SOP gate
Browser (mutually suspicious referee) Origin A: https://bank.example:443 Document + running script Cookie: sid=8f2a... (HttpOnly, Secure) Can read: its own DOM, its own cookies Cannot read: origin B's response body Origin B: https://evil.example:443 Attacker-controlled script fetch("https://bank.example/api") --> Request IS sent, cookie IS attached Response body: BLOCKED by SOP SOP Gate origin match? Server: bank.example Session store lookup by sid Trusts cookie presence as identity

Complexity & correctness

Time/space complexity of the mechanisms. The engineering complexity here is not algorithmic in the classical sense; it is about the cost of the checks performed on every request, and about the entropy budget of the identifiers used.

  • Origin comparison (`SAME_ORIGIN`) is an O(1)O(1) string/tuple comparison of scheme, host, port — negligible cost, performed on essentially every cross-context access attempt (script property access, `fetch`, `postMessage` handler, iframe access). This is why SOP can be enforced on every single DOM access without perceptible overhead: it is a constant-time gate, not a search.
  • Cookie attachment is O(k)O(k) per request, where kk is the number of cookies in the jar whose domain could plausibly match; browsers index cookies by registrable domain so this is effectively a small constant in practice, not a scan of the whole jar.
  • Session lookup is O(1)O(1) amortized with a hash-table-backed session store keyed by session ID (or O(log⁡n)O(\log n) with a balanced-tree-backed store), where nn is the number of live sessions.
  • Space: the browser stores O(number of cookies)O(\text{number of cookies}) client-side (bounded per-domain by browser limits, typically ~50 cookies / ~4KB each per domain); the server stores O(n)O(n) session records for nn concurrent sessions, or O(1)O(1) per request if sessions are instead encoded as signed/encrypted stateless tokens (at the cost of harder revocation).

Correctness argument for SOP as an access-control invariant. Treat SOP as maintaining a security invariant over the lifetime of a page: > Invariant. At any point during execution, a script executing in a document with origin OO can obtain the plaintext contents (DOM properties, response bodies, storage) of a resource with origin O′O' only if O=O′O = O' or O′O' has explicitly granted access to OO. This invariant is established at document creation (a document's origin is fixed from its URL at load time, with narrow, well-defined exceptions such as `document.domain` relaxation for pre-registrable-domain-suffix cases) and is preserved by construction because every browser API that could leak cross-origin data — `XMLHttpRequest.responseText`, `iframe.contentWindow.document`, `localStorage`, reading pixel data from a cross-origin `` image — is required by the browser specification to perform the `SAME_ORIGIN` check (or an opt-in header check) before returning data, and to return an opaque/error value otherwise. Because the check is applied at every one of these API boundaries rather than once at page load, the invariant holds not just initially but at every subsequent step, which is exactly the inductive structure needed for an invariant-based correctness argument: base case at load, preservation at every state-transition (API call). The invariant explicitly does not cover request initiation. A `

` submitted from `evil.example` still fires, cookies still attach (subject to `SameSite`), and the bank's server still processes it if it cannot distinguish "user clicked a button on my own site" from "user's browser was tricked into submitting this form" — this is precisely the CSRF gap, and understanding it as a design boundary rather than a bug is the right mental model: SOP was built to protect confidentiality of cross-origin reads, not to guarantee the integrity/intent of cross-origin writes.

Honest tradeoffs.

  • Storing sessions server-side (O(n)O(n) memory) gives instant revocation (`SESSION_STORE.delete(sid)`) but does not scale statelessly across many servers without a shared store.
  • Stateless signed tokens (e.g., JWTs) remove server-side storage entirely but make revocation hard: a compromised token remains valid until expiry unless you maintain a blocklist, which reintroduces the O(n)O(n) state you were trying to avoid.
  • `SameSite=Strict` closes the CSRF-via-automatic-cookie gap almost entirely but breaks legitimate cross-site flows (e.g., arriving at a logged-in page via an external link), which is why `Lax` is the practical default rather than `Strict`.

Worked example

Trace a concrete session across three requests, showing exactly what the browser stores, what it sends, and where SOP intervenes.

Setup. User visits `https://mail.example` and logs in. Simultaneously, a script hosted at `https://ads.example` is embedded via `

Pioneers & history

The web security model was not designed in one document; it accreted as browsers, protocols, and attacks co-evolved. Its central figure is Lou Montulli, the Netscape engineer who invented the HTTP cookie in 1994 to solve a mundane e-commerce problem — remembering a shopping cart across stateless requests — without anticipating that this mechanism would become the load-bearing wall of web identity, and consequently the single highest-value target in browser security for the next three decades. The same-origin policy itself has no single named inventor; it emerged from Netscape's early JavaScript security engineering (circa 1995) as an ad hoc but essential restriction, later formalized across browser vendors and eventually specified rigorously by the web standards bodies as browsers converged on interoperable behavior.

Origins · 1 minLou Montulli and how this was discovered
LM
Lou Montulli
born 1968 · Netscape Communications, mid-1990s browser wars

Montulli co-authored the original HTTP cookie specification while at Netscape, intending it purely as a mechanism for e-commerce state (shopping carts) on an otherwise memoryless protocol. The cookie's later role as the near-universal carrier of session identity, and hence as the central artifact that the same-origin policy and CSRF/XSS defenses all revolve around protecting, was an emergent consequence rather than an original design goal — a recurring pattern in security history where a convenience mechanism becomes a trust primitive.

History
1994
— Lou Montulli implements the HTTP cookie at Netscape for stateful shopping carts.
1995
— Netscape ships JavaScript alongside an informal same-origin restriction to stop pages from reading each other's data.
2000
— Cross-site scripting is named and catalogued as a distinct vulnerability class, exposing SOP's blind spot for injected same-origin script.
2011
— The `SameSite` cookie attribute is proposed, giving browsers a built-in, opt-in mitigation for the SOP's send/read asymmetry that enables CSRF.
2016
onward — Browsers move `SameSite=Lax` toward a secure-by-default posture, tightening the historical CSRF gap without eliminating the need for explicit anti-CSRF tokens.

Where this matters

Every authenticated web application — banking portals, webmail, social media, cloud consoles, SaaS dashboards — depends on this exact model: a cookie-carried session validated by the SOP boundary that keeps other open tabs from reading it directly. Browser vendors (Chromium, Firefox, WebKit) implement SOP and cookie attribute enforcement as core, security-critical browser engine code, and any divergence between engines historically has been treated as a serious interoperability bug. Content Security Policy, Cross-Origin Resource Sharing, and `postMessage`-based cross-frame communication are all designed as controlled, explicit exceptions to SOP, layered so that sites can cooperate deliberately without SOP being weakened by default. Single-sign-on systems (OAuth, SAML) are essentially elaborate protocols for safely moving identity across origin boundaries that SOP otherwise refuses to bridge implicitly. Security teams performing web application penetration tests spend a large fraction of their time specifically probing this boundary — checking cookie flags, testing for SOP bypasses, and validating CSRF protections — because it is the highest-leverage place in a web application's architecture to find a critical vulnerability.

Ask the tutor: How exactly does a Content-Security-Policy header or CORS response header act as a controlled, explicit exception to the same-origin policy without weakening it for everyone else?

Why doesn't the same-origin policy just block cross-origin requests entirely?

Because the web was built on cross-origin resource loading — images, stylesheets, scripts, iframes, and API calls to third-party services all depend on it working. Blocking all cross-origin requests would break the web as designed. Instead, SOP takes a narrower, more surgical approach: it lets the request go out, but restricts what the requesting script is allowed to read back. A page can display a cross-origin image or load a cross-origin script, because those don't hand raw response data to arbitrary script by default. But `fetch()`ing a cross-origin JSON API and reading the body requires the target server to explicitly opt in via CORS headers, precisely because that would otherwise leak private data to any page that requests it.

Are cookies and sessions the same thing?

No — a cookie is a small piece of data the browser stores and resends; a session is server-side state. The cookie usually contains nothing but a random session identifier; it is a pointer, not the data itself. The server maintains the actual session record (user ID, roles, timestamps) in its own store, indexed by that identifier. This separation matters for security: if the identifier is stolen, the attacker only gets a pointer they can present, not the data directly, and the server can invalidate that pointer instantly by deleting the session record, which is exactly why server-side session stores support fast, reliable logout and revocation.

Why is HttpOnly considered separate from the same-origin policy?

They defend against different attackers. SOP defends against a different origin's script trying to read your origin's data — it's an inter-origin boundary. `HttpOnly` defends against a script running within your own origin (for instance, injected via an XSS vulnerability) trying to read `document.cookie`. SOP would not stop that injected script at all, because as far as the browser is concerned, it's running with your page's own origin and permissions. `HttpOnly` closes this specific gap by making the cookie invisible to any JavaScript, same-origin or not, so even a successful same-origin script injection cannot directly exfiltrate the raw session token through the DOM cookie API.

What exactly does SameSite=Strict break, and why isn't it the universal default?

`SameSite=Strict` withholds the cookie even on top-level navigations arriving from another site — so if a user clicks a link from an email client or another website straight into your logged-in app, the very first request lands as if logged out, because the cookie isn't sent on that cross-site navigation. This breaks common, legitimate flows: shared links, payment redirects returning from a third-party processor, and social login callbacks. `SameSite=Lax` was chosen as the safer default because it permits these top-level "safe" navigations while still blocking the cross-site POSTs and subresource requests that CSRF actually relies on, giving most of the protection without breaking ordinary browsing.

If SOP protects localStorage from other origins, why is storing session tokens there discouraged?

Because the realistic threat to a session token is rarely "a different origin reading it directly" — SOP already stops that. The realistic threat is a same-origin XSS bug: an attacker manages to run script inside your own origin, perhaps via an unsanitized comment field. That injected script has exactly the same access to `localStorage` as your legitimate application code does, because SOP treats them identically — same origin, same privileges. An `HttpOnly` cookie is invisible to all JavaScript regardless of where it came from, which is a strictly stronger property in the XSS threat model than anything storage-API-based can offer.

How does the browser decide which cookies to attach to an outgoing request?

It checks each stored cookie's scoping attributes against the target request: the `Domain` (and any subdomain rules), the `Path` prefix, whether the cookie is marked `Secure` (requiring HTTPS), and the `SameSite` policy relative to whether the request is same-site or cross-site and what kind of request it is (top-level navigation vs. subresource fetch vs. form POST). Only cookies passing every applicable check get concatenated into the outgoing `Cookie` header. This entire matching process happens automatically and silently before any application code runs, which is exactly why cookie-based sessions are convenient — and exactly why misconfigured attributes cause security failures invisibly, since nothing in the application layer needs to "ask" for the cookie to be attached.

Is HTTPS alone enough to make the web security model safe?

No — HTTPS protects data in transit between browser and server against eavesdropping and tampering by network attackers, which is necessary but addresses a different threat than SOP or session hygiene. Even over a perfect TLS connection, a malicious script from another origin can still try to read your cookies or DOM if SOP is misconfigured or bypassed, and an XSS bug can still steal a non-`HttpOnly` cookie regardless of transport encryption. HTTPS, SOP, and cookie attributes are complementary, addressing network-level, cross-origin-level, and same-origin-injection-level threats respectively — dropping any one leaves the corresponding threat fully open.

Before this

Where this leads