The Browser Security Model
The origin as the unit of trust, the renderer as a sandbox, and four separate mechanisms answering four separate questions — mixing them up is why security fixes so often do nothing.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
What does the browser actually enforce on my behalf, and which mechanism answers which question?
A person opens a tab, then another. They expect the first page to be unable to read the second, and they expect it without having ever heard the word "origin".
Security belongs to the backend. The frontend adds whatever header the pen test asked for, and the browser deals with the rest.
The single most damaging frontend vulnerability — script injected into your own page — runs *inside* every boundary the browser enforces. It is same-origin by construction, so the origin model does nothing about it (Cross-Site Scripting).
- The single most damaging frontend vulnerability — script injected into your own page — runs *inside* every boundary the browser enforces. It is same-origin by construction, so the origin model does nothing about it (Cross-Site Scripting).
- Adding a header to the wrong mechanism is the standard failure. Teams add permissive CORS headers hoping to fix an authorization problem, and end up loosening the only thing that was working.
- A rule enforced only in the client is a suggestion. Every request the browser sends can be sent again by a tool that ignores every header you set (What the Frontend Is Responsible For in Auth).
- "The browser handles it" and "we shipped a policy" are different claims. A
Content-Security-Policywithunsafe-inlinein the script directive is a header that exists and a defence that does not (Content Security Policy). - The boundary moves under you. Third-party cookie behaviour and storage partitioning are being tightened by browsers on independent schedules, so a feature that works everywhere today is a support ticket next year (Storage Security and Durability).
What is actually happening
In the browser, not in the framework.
- The origin — scheme, host and port together — is the unit of isolation. Two documents share an origin or they do not; there is no partial match, and a subdomain is a different origin (Origins and the Sandbox).
- The site is a coarser unit — roughly scheme plus registrable domain — and it is what cookies and, increasingly, storage partitioning key on.
app.example.comandapi.example.comare cross-origin but same-site, which is exactly the distinction that makesSameSitecookies useful (Cookies). - The renderer is sandboxed: the process that parses your HTML and runs your JavaScript cannot open a file, bind a socket or draw outside its tab. It asks a more privileged browser process for everything, and that process re-checks (The Multi-Process Browser).
- A secure context is a further gate. Service workers,
crypto.subtle, geolocation, clipboard read, shared memory and more simply do not exist on an insecure origin, regardless of what the user permits.localhostis treated as secure so that development is possible. - On top of that sit four mechanisms answering four different questions: who may read (same-origin policy), may script see this cross-origin response (CORS), what may execute or load at all (CSP), and what gets sent (cookie attributes). They compose; none replaces another.
- Everything above is *browser* enforcement. It applies to a browser and to nothing else — which is why the server has to repeat every check that matters (What the Frontend Is Responsible For).
What this makes the browser do
And which of it is avoidable.
- A security check on essentially every boundary crossing: each subresource load is matched against the policy, each cross-origin property access is checked against the origin, each cookie is matched against its attributes and the request context.
- CSP evaluation on every inline script, style,
eval, image, connection and frame — cheap per check, but a large policy with many directives and a page with thousands of subresources is real parse and match work (Content Security Policy). - Preflight
OPTIONSrequests for cross-origin calls the browser cannot classify as simple, which is a full extra round trip before the real request starts (CORS). - Process allocation: isolating a site into its own renderer costs memory, which is why the strategy differs between browsers and adapts to the device.
- Avoidable work: preflights you triggered with an unnecessary custom header, and CSP violations being generated and reported at high volume because an extension or a vendor tag is fighting the policy on every page view.
Origin, site, and why both exist
Almost every confusing browser restriction resolves once you can say which of two units of trust is being applied. The origin is scheme, host and port together, and it is what governs reading: DOM access, storage, and whether script may see a response. The site is coarser — roughly scheme plus registrable domain — and it is what cookies and storage partitioning key on.
The pair matters because the common corporate topology, an app and an API on sibling subdomains, is *cross-origin* and *same-site* at the same time. That single fact explains why such a setup needs CORS configuration and why a SameSite=Lax cookie still rides along on requests between them.
- There is no partial origin match. Nothing about
app.example.comgrants access toexample.com. localhostand127.0.0.1are different origins, which is a genuinely common source of a lost afternoon.file://URLs get their own, deliberately restrictive treatment, and the details vary by browser — testing security behaviour by opening a file is testing something else.
BASE https://app.example.com/dashboard https://app.example.com/settings SAME ORIGIN scheme, host, port all match https://app.example.com:443/x SAME ORIGIN 443 is the default for https http://app.example.com/dashboard cross-origin scheme differs https://api.example.com/orders cross-origin host differs https://app.example.com:8443/x cross-origin port differs https://example.com/ cross-origin host differs; a parent is not a wildcard ...but cookies and storage partitioning key on the SITE, which is coarser: site(https://app.example.com) = (https, example.com) site(https://api.example.com) = (https, example.com) <- SAME SITE so app. and api. are CROSS-ORIGIN and SAME-SITE simultaneously. Reads are blocked between them. SameSite=Lax cookies are not.
Four questions, and the mechanism that answers each
The mush this module exists to prevent is treating "browser security" as one thing with one fix. It is a set of independent mechanisms, each answering one question, each configured somewhere different, and each with a specific thing it will not do. A fix aimed at the wrong one is not a partial fix; it does nothing at all.
Read the last column as the diagnostic. When a proposed fix is in that column, the mechanism has been misidentified, and the real bug is still there after the change ships.
| If you want to... | The mechanism | Delivered as | The wrong tool people reach for |
|---|---|---|---|
| Stop another origin reading this page's DOM, storage or responses | Same-origin policy | Nothing — it is the default and cannot be switched off | CORS headers, which loosen this boundary rather than tighten it |
| Let one named other origin read your API responses in a browser | CORS | Access-Control-Allow-* headers, from the responding server | A dev proxy, which hides the boundary in development and not in production |
| Stop injected markup from executing even when it reaches the page | Content Security Policy | A response header, or a meta element with real limits | Output escaping alone — necessary, but it is the first layer, not the second |
| Stop a cookie riding along on a cross-site request | SameSite, plus Secure, HttpOnly, Domain, Path | Set-Cookie from the server, plus browser defaults | A CSRF token, which is complementary rather than a substitute |
| Stop your page being framed and clicked through | CSP frame-ancestors, or X-Frame-Options | A response header on the page being framed | Frame-busting JavaScript, which a framing page can often neutralise |
| Stop a modified third-party file from executing | Subresource Integrity | An integrity attribute on the tag, with crossorigin | Pinning a version, which does not detect a changed file at the same URL |
Where the browser stops
The last thing to internalise is the shape of the gap. The browser enforces its rules perfectly and enforces nothing else, and the mistakes that follow from forgetting that are the most expensive ones in this module. Each row below is a real production failure pattern rather than a hypothetical.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| A rule exists only in client code | A user performs an action the UI never offered | The client was treated as an enforcement point; it is a rendering of one | Enforce server-side and render the consequence. The two are not alternatives (Authorization-Aware UI). |
| Injected script runs in your page | Session data and DOM contents leave the origin | The injected code *is* your origin, so no origin-based control applies | Escape at render time, sanitize what must be HTML, and use CSP to cap the impact (Cross-Site Scripting). |
| A secret is in the bundle | The key appears in someone else's tool | Everything shipped to a browser is readable, minified or not | Move the call server-side. There is no client-side place to hide a credential (Backend for Frontend). |
A powerful API is undefined | A feature works locally and throws in a preview environment | The API is gated on a secure context, and the environment is served over plain HTTP | Serve every environment over TLS. localhost is exempt precisely so this is possible (Deploying a Frontend). |
| A tightened policy has no effect for some users | Violation reports keep arriving from the old shape | Long-lived tabs and a service worker are still serving the previous document | Version the policy with the deploy and plan for clients that update on their own schedule (Long-Lived Clients and Version Skew). |
How to build it
Most important first.
- Decide the origin topology first. Whether the API is on the app origin, a sibling subdomain, or somewhere else determines your CORS story, your cookie story and your CSRF story simultaneously (Origins and the Sandbox).
- Treat escaping as the primary defence and every header as a second layer. CSP exists to reduce the impact of a bug you did not find; it is not a reason to stop looking (Sanitization and Trusted HTML).
- Say out loud, per rule, which side enforces it. "The button is hidden and the endpoint rejects it" is a design; "the button is hidden" is a wish (Authorization-Aware UI).
- Serve everything over a secure transport, including development where you can, because half the platform is unavailable otherwise and discovering that late is expensive (The Service Worker Lifecycle).
- Inventory what executes in your page. Every dependency and every vendor tag runs with the full authority of your origin, and that inventory is the actual perimeter (Third-Party Scripts and the Supply Chain).
- Turn violations into signal. Report-only policies, CSP reports and client error tracking are how you learn what is running in browsers you do not own (Frontend Error Tracking).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- A restrictive CSP can break assistive technology that injects script or inline style into the page. Some screen-reader helpers, translation layers and extension-based readers work exactly that way, and the failure is silent for everyone except the user who depends on it (Content Security Policy).
- Security failures need announcing, not just rendering. A blocked widget that leaves an empty region tells a sighted user "something is missing" and tells a screen-reader user nothing at all (Live Regions and Announcement).
- Frame-based defences interact with keyboard navigation: a page that refuses to be framed, or breaks out of a frame, moves focus in ways that can strand a keyboard user mid-flow (Focus Management).
- Never let a security banner or interstitial be a focus trap without an escape. An unavoidable modal that cannot be dismissed by keyboard is an outage for the people who cannot dismiss it another way (Keyboard Operability).
What can go wrong
- Mechanism confusion: a permissive
Access-Control-Allow-Originadded to fix what was actually a cookie or an authentication problem, leaving the original bug and adding a new exposure. - A policy that is present and inert.
unsafe-inlineinscript-src, or a nonce reused across responses, produces a header that passes an audit checklist and blocks nothing (Content Security Policy). - A defence that only holds in one browser. Frame-busting script,
X-Frame-Optionsvariants and storage partitioning all behave differently across engines (Clickjacking and Framing). - Trusting the sandbox to contain an injection. Injected script is your origin; it reads your storage, your DOM and any cookie not marked
HttpOnly. - The mitigation failing: a strict policy blocks a legitimate vendor tag, someone loosens it in a hotfix under pressure, and nobody tightens it again.
- A policy delivered by a
metaelement applies from where the parser reaches it; anything already parsed above it was never covered. Header delivery has no such window (Tree Construction). - A service worker registered by an earlier version of the site can still be serving cached responses when a new policy ships, so the tightened header never reaches the page (The Service Worker Lifecycle).
- Cookie attributes and partitioning changes roll out to browser populations gradually, so two users on the same version of your app can be on two different rule sets on the same day.
- The browser enforces origin isolation, cookie attributes, CSP and the secure-context gate absolutely. It enforces nothing else, and it will happily execute any code that legitimately reached your page.
- Everything shipped to the client is public. Minification is not obfuscation, a bundled configuration object is readable, and an API key in a frontend build is a published API key (Bundle Analysis).
- An attacker who executes script in your origin has your origin: your DOM, your
localStorage, your same-originfetchcalls with cookies attached. That is why injection outranks almost everything else here (Cross-Site Scripting). - These mechanisms raise cost and shrink blast radius. Defence in depth is the frame: no single layer is load-bearing on its own.
- "The browser sandbox protects my users from my bugs." It protects users from *other sites*. Script running in your page is inside the sandbox with you.
- "CORS secures the backend." It does not secure anything. It is a browser policy governing whether script may read a cross-origin response, and a non-browser client is entirely unaffected (CORS).
- "We have a CSP, so XSS is handled." A policy limits impact. Escaping prevents the bug. Doing only the first means shipping the vulnerability and hoping the mitigation holds (Content Security Policy).
- "HTTPS means we are secure." Transport security means the bytes were not read or altered in flight. It says nothing about what those bytes do once parsed (Why TLS Exists).
- "Subdomains are the same origin." They are not — but they may be the same *site*, and confusing the two produces cookie bugs that look like authentication bugs (Cookies).
Measuring it, and what changes in the field
- The Security panel in Chromium-based devtools reports the connection, the certificate and the origins involved in a page — a fast way to see what the page actually loaded from (A Mental Model of the Devtools).
- CSP violation reports collected from the field are the only view of what real browsers, with real extensions, actually blocked (Content Security Policy).
- The Network panel distinguishes a preflight from the real request, and shows which cookies were sent and, importantly, which were withheld and why (Debugging the Network).
- The Application panel enumerates storage, cookies with their attributes, and registered service workers — the persistent state that outlives your page (Persistent Client State).
- On a page with an unusual number of subresources or a very large policy, CSP matching and preflighting become measurable rather than free.
- On a high-latency network the extra preflight round trip is felt directly by the user, while on a local network it is invisible — the classic reason this is discovered in production.
- In browsers with tracking protection enabled, or in private windows, third-party storage and cookies behave differently enough that a feature can work for most users and be broken for a large minority.
- In a long-lived tab, headers are whatever was served when the document loaded. A tightened policy does not reach a tab that has been open since yesterday (Long-Lived Clients and Version Skew).
- Strict isolation and a strict policy unlock capabilities and lock out embeds. Cross-origin isolation buys you precise timers and shared memory and costs you every third-party frame that has not opted in (Shared Memory and Cross-Origin Isolation).
- Same-origin API removes CORS, preflights and much of the CSRF conversation, and couples the deployment of app and API (How API Shape Drives UI Complexity).
- Every layer added is a layer that can break a legitimate feature at an inconvenient time, which is why report-only rollout exists and why it takes longer than shipping the header.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe origin model, the same-origin policy, the secure-context gate and the existence of CSP and cookie attributes are specified and consistent across Blink, Gecko and WebKit; the differences are at the edges rather than in the rules themselves.
- BROWSER-SPECIFICProcess isolation strategy differs: Chromium gives each site its own renderer process by default, while Firefox uses a bounded pool of content processes, so memory footprint and crash isolation differ even though the security model a developer programs against does not.
- SPEC-EVOLVINGThird-party cookie behaviour,
SameSitedefaults and storage partitioning are mid-transition and on different timetables in Safari, Firefox and Chromium. Treat any current behaviour as a snapshot, and verify against the specification rather than against a tutorial written two years ago.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Software Design — where a security decision belongs in a client application's module structure, and why a check scattered across components is a check nobody can audit.
- — Testing & Reliability Engineering — what a test can actually prove about a browser-enforced control, given that the enforcement lives outside your code.