Skip to content

Chapter 31: Privacy and the Modern Browser Security Model

The browser is, among other things, a security boundary.

It runs code from many sources — the application’s own code, the third-party scripts the application embeds, the cross-origin iframes other companies have asked the application to include, the user’s own browser extensions, the operating system’s own injected code, and a long list of others — inside a single process tree that has to keep them from interfering with each other and with the user’s data. The accumulated work the browser does to maintain this boundary is, in 2025, one of the platform’s most-developed surfaces. It’s also, for frontend architecture, one of the most-ignored.

This chapter walks through the platform’s security and privacy commitments — what the browser now enforces, what it permits, what it prohibits, and what application code has to do to work within the model. The work is mostly done by the browser; the application’s job is to understand the rules and not fight them.

The foundation is older than most of the rest of this book.

Netscape introduced the same-origin policy in Navigator 2.0 in 1995. The rule, in its modern form, says that JavaScript running on one origin (the protocol + host + port triple) can’t read content from another origin without explicit permission. A page on app.example.com can’t make a request to api.bank.example.com and read the response. A page on https://example.com:443 can’t read content from https://example.com:8080. Even subdomains are separate origins by default.

The policy is what makes the web usable for sensitive activity. Online banking, healthcare portals, government services, anything that authenticates the user — all of these depend on the same-origin policy to prevent malicious sites from making requests to the user’s authenticated session on a different site. Without the policy, the user’s banking session cookie would be readable by any random page they visited.

Same-origin is the default deny. Everything that loosens it — CORS, postMessage, document.domain (now mostly deprecated), Window.opener — is a specific, controlled relaxation. The application doesn’t get to choose whether same-origin applies; the browser enforces it. The application chooses which loosenings to opt into.

Most applications need at least some cross-origin requests — a frontend at app.example.com calling an API at api.example.com, a website embedding fonts from fonts.example.com, an analytics service receiving events from any of a thousand origins. CORS (Cross-Origin Resource Sharing) is the platform’s mechanism for letting the API decide which origins are allowed to read its responses.

The mechanism is HTTP headers. The browser sends a request with an Origin header indicating where the request came from. The server responds with Access-Control-Allow-Origin indicating which origin (or * for any origin) is allowed to read the response. If the headers don’t match, the browser blocks the JavaScript from reading the response — the request still went out, but the application sees an error.

For non-simple requests (anything other than GET/POST with simple content types), the browser sends a preflight OPTIONS request first to verify the server allows the actual request method. The preflight handshake adds latency and is the source of many why does my CORS request fail debugging sessions.

For frontend architecture, the CORS rules mean three things. The application has to either be on the same origin as its APIs, or the APIs have to explicitly allow the application’s origin. Cross-origin authentication is harder than same-origin (cookies need SameSite settings, credentials need explicit handling). And the production deployment story has to account for CORS — putting the API and the frontend on the same domain (or origin, ideally) is often the simplest move.

CSP is the application’s tool for restricting what code the browser will execute on its pages.

A typical CSP header looks like this:

Content-Security-Policy: default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';

Each directive controls a category of resource. script-src restricts which scripts the page can load. style-src restricts stylesheets. connect-src restricts what fetch/XHR can reach. frame-ancestors restricts which pages can embed this one in an iframe (replacing the older X-Frame-Options header).

CSP is, in security terms, a defense in depth mechanism. Even if an attacker manages to inject code into the page (XSS), CSP can prevent the injected code from doing anything useful — connecting to attacker-controlled servers, loading additional scripts, executing inline event handlers. A strict CSP turns most XSS vulnerabilities from full account takeover into nuisance defacements.

The cost is that CSP requires the application to know its own resource graph. Every script source, every style source, every image domain, every API endpoint, every embedded iframe — all of them need to be enumerated in the policy. Applications that load resources dynamically (analytics SDKs, third-party widgets, payment processors) tend to have CSP policies that drift toward permissive over time, as each new dependency requires another allowed source.

A strict CSP is one of the highest-leverage security improvements an application can make. The investment is real (catalog every resource, set up the policy, debug what’s broken, maintain it as dependencies change). The payoff is substantial protection against XSS, which is consistently one of the OWASP Top 10 vulnerabilities.

A specific feature worth knowing about because the supply-chain chapter (Ch 17) made it relevant.

Subresource Integrity (SRI) is a way to verify that a third-party script or stylesheet hasn’t been tampered with. The application includes the expected hash of the resource in the HTML:

<script src="https://cdn.example.com/lib.js"
integrity="sha384-..."
crossorigin="anonymous"></script>

The browser fetches the script, computes its hash, and refuses to execute it if the hash doesn’t match. A compromised CDN, a polyfill.io-style domain takeover (Chapter 17), or any other supply-chain attack that modifies the script in transit gets blocked.

SRI is straightforward to add for static third-party resources. It’s harder for resources that update frequently — the hash has to be updated every time the resource changes. For CDN-hosted versions of stable libraries (jQuery’s CDN releases, specific Lit versions, design-system distributions), SRI is a small but real defense.

The third-party cookie deprecation has been one of the most-discussed platform changes of the past several years.

A third-party cookie is a cookie set by a domain other than the one the user is currently visiting. The classic use case is cross-site tracking — an ad network’s cookie is set when the user visits any site that includes the ad network’s script, and the same cookie is sent every time the user visits another site that includes the same script. The network can stitch together the user’s browsing across many sites.

The platform’s commitment is to phase this out. Safari’s Intelligent Tracking Prevention (ITP), introduced in WebKit by John Wilander’s team in 2017, was the first major implementation. ITP started by limiting third-party cookies’ lifetime to 24 hours and then to seven days, and now blocks third-party cookies entirely in most contexts. Firefox followed with its Total Cookie Protection, which partitions cookies per top-level site (the same cookie value reads differently depending on which site the user is visiting). Chrome’s third-party-cookie deprecation has been slower and more contested, with multiple delays and the Privacy Sandbox proposals as the company’s planned replacement.

For frontend architecture, the third-party cookie deprecation has specific implications. Authentication that depends on cross-origin cookies stops working in Safari and Firefox. Single sign-on systems that bounce the user across multiple origins need to use the Storage Access API (added 2020, refined since) to request explicit user permission. Analytics tools that depend on cross-site tracking either lose data or migrate to first-party data collection. Advertising integrations that depended on cross-site cookies are being replaced by various Privacy Sandbox APIs (Topics, FLEDGE, attribution reporting).

The application’s job, in 2025, is to assume third-party cookies don’t work and design around that assumption. First-party cookies (cookies set by the origin the user is currently on) are still fully supported and the right place for authentication, session management, and most application state.

Chrome’s Privacy Sandbox is a collection of proposed APIs for handling the use cases that third-party cookies enabled, without the cross-site tracking they made possible.

The components include Topics (the browser categorizes the user’s interests based on browsing, advertisers receive coarse categorization without per-user identifiers), FLEDGE / Protected Audience API (ad auctions run in-browser without sending the user’s data to the ad network), Attribution Reporting (advertisers learn that a conversion happened without learning which user converted), and several others.

The work has been controversial. Privacy advocates argue Privacy Sandbox preserves too much of the advertising industry’s tracking capabilities, repackaged in less obviously invasive ways. Some advertisers argue Privacy Sandbox doesn’t preserve enough capability and represents a competitive disadvantage. The UK Competition and Markets Authority has been overseeing Google’s commitments to the broader ecosystem. The rollout has been delayed multiple times.

For most frontend developers, Privacy Sandbox is something to know exists rather than something to actively engage with day-to-day. The architectural takeaway is that the platform’s posture is increasingly no cross-site tracking by default. Anything an application wants to do that crosses site boundaries — advertising, analytics, federated identity, social embeds — needs to find a way to do it within the constraints the browser is increasingly enforcing.

Permissions Policy (formerly called Feature Policy) is the application’s tool for restricting what its own pages (and embedded iframes) can do.

Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=()

The header above says: this page can’t use the camera or microphone at all, can use geolocation only on its own origin, and can’t use the Payment Request API. The browser enforces these restrictions; even if a script in the page tries to call navigator.mediaDevices.getUserMedia({ video: true }), the call will fail because the policy disallows it.

The use cases are real. An application can lock down its own attack surface by disabling features it doesn’t use. A page that embeds third-party iframes can restrict what those iframes are allowed to do. A site with a strict security posture can prevent unexpected hardware access, geolocation requests, and similar permission-gated features.

The policy is per-page, set through HTTP headers, and inherited (with restrictions) by iframes. The mechanism is one of the platform’s better defense-in-depth tools.

The authentication story has been shifting toward WebAuthn and Passkeys.

WebAuthn (Web Authentication, a W3C standard) lets the browser handle authentication through public-key cryptography rather than shared secrets (passwords). The user’s device (phone, computer, hardware security key) generates a private key during registration. The site stores the matching public key. Future authentications happen by the device proving it has the private key, with biometrics or a PIN unlocking the local credential.

Passkeys are the consumer-facing name for synced WebAuthn credentials. Apple’s iCloud Keychain syncs passkeys across the user’s Apple devices. Google’s Password Manager syncs them across Google devices. Microsoft’s Authenticator syncs them across Microsoft devices. The user creates a passkey on one device and uses it on all their other devices automatically.

For frontend applications, WebAuthn is increasingly the right authentication primitive. The user doesn’t pick a password (so they can’t pick a bad one, can’t reuse it, can’t be phished into giving it to an attacker). The credential is bound to the origin (so a phishing site can’t accept the user’s credential). The cryptography happens on the user’s device (so the server never sees the secret).

Implementing WebAuthn is non-trivial — the protocol has subtleties, the UX of falling back when passkeys aren’t available is delicate, and the server has to maintain a database of public keys per user. Libraries like SimpleWebAuthn (Matthew Miller) make the implementation tractable. Major identity providers (Auth0, Clerk, Stytch, the platform identity tools at AWS / Google / Microsoft) increasingly include passkey support out of the box.

The architectural point is that passwords are a legacy authentication mechanism. New applications, designed in 2025 and shipping in 2026, should be built around WebAuthn first with password fallback rather than the other way around. The platform supports it. The user experience is better. The security profile is dramatically improved.

A more specialized but important security area, worth naming briefly.

Some platform capabilities — SharedArrayBuffer, high-resolution performance.now(), the various measurement APIs — are powerful enough to enable side-channel attacks like Spectre. The platform mitigates the risk by gating these capabilities behind cross-origin isolation.

A page is cross-origin isolated when it sets both:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

COOP (Cross-Origin-Opener-Policy) prevents cross-origin windows from sharing browsing context. COEP (Cross-Origin-Embedder-Policy) requires that every cross-origin resource the page loads opt into being embeddable (via the Cross-Origin-Resource-Policy header).

When both are set, the page enters cross-origin isolated mode and gains access to the powerful APIs. The cost is that loading cross-origin resources without the right headers fails — the page becomes more restricted in what it can embed.

Most applications don’t need cross-origin isolation. The applications that do (heavy WASM workloads, real-time audio/video processing, certain kinds of game engines, scientific computing) usually know they need it. The architecture worth knowing: the platform reserves its most powerful capabilities for applications that opt into the strictest isolation, on the theory that security and capability are linked.

What This Means for Application Architecture

Section titled “What This Means for Application Architecture”

The platform’s security and privacy positions have direct implications for how applications should be built.

Assume same-origin. Put the frontend, the API, and most static assets on the same origin if possible. Cross-origin work is supported but adds friction. The same-origin default is the simplest path.

Use a strict CSP. Catalog the resources the application actually uses. Disallow inline scripts and inline styles where possible (or use nonces / hashes for the inline blocks that are unavoidable). Avoid unsafe-eval. Set the policy in a Content-Security-Policy header from the server.

Set Permissions-Policy. Disable features the application doesn’t use. The defense-in-depth value is real.

Use SRI for third-party CDN resources. The hash check is cheap and catches a category of supply-chain attack the rest of the dependency-management toolchain doesn’t.

Plan for the no-third-party-cookie world. Authentication, analytics, and any cross-site behavior should work without third-party cookies. The Storage Access API exists for the cases that really need cross-origin storage access; use it explicitly and request user permission.

Move toward passkeys. New authentication flows should support WebAuthn from the start. The user experience improvement is meaningful; the security improvement is substantial.

Pay attention to subresources. Every npm dependency, every CDN-loaded script, every embedded iframe is part of the application’s security surface. The supply-chain risks from Chapter 17 and the security-policy work in this chapter are the same problem from different angles.

Kitsune’s architecture aligns with these positions. The component library doesn’t require inline scripts. The runtime doesn’t depend on cross-origin cookies for authentication (the application’s auth strategy is its own, but the runtime works well with same-origin token-based authentication). The framework’s bundle is small enough that subresource integrity checks are practical. The decisions accumulate into an application that’s easier to secure than the framework-heavy alternative.

The next chapter takes the platform-reaching-into-native-territory angle — WebAssembly as a portable second runtime, web workers and SharedArrayBuffer for real parallelism, the hardware-access APIs (WebUSB, WebBluetooth, WebSerial, WebMIDI, WebHID) that let the browser interact with physical devices, and the broader story of the platform expanding into capabilities that used to require native applications.

Pick an application you have access to. Inspect its current security posture:

  1. What’s the Content Security Policy? (Check the response headers in browser dev tools. If there’s no CSP header, that’s the first finding.)
  2. What’s the Permissions-Policy? (Same check — many applications don’t set this at all.)
  3. List the third-party origins the application loads from. (Scripts, styles, images, fonts, iframes, analytics endpoints.)
  4. For each third-party origin, does the application use Subresource Integrity? (Look at <script> and <link rel="stylesheet"> tags for integrity attributes.)
  5. How does the application handle authentication? Passwords-only? OAuth with the major providers? WebAuthn / passkeys?
  6. Does the application depend on third-party cookies? Test it in Safari (which blocks them by default) — does it still work?
  7. If the application has user-generated content, what’s the strategy for XSS prevention? Sanitization on input? Sanitization on output? Trusted Types?

Each of these questions exposes a piece of the application’s actual security posture. Most applications come out looking weaker than the team’s mental model would suggest. The fixes are usually well-documented and small individually. The cumulative effect is a substantially harder application for an attacker to exploit.

The goal isn’t to terrify the team. The goal is to develop a working sense of the platform’s security model and to use the affordances it provides. The platform has been doing significant work on the application’s behalf for decades. Most of the work is invisible until someone tries to attack the application. The architecture’s job is to keep the platform’s protections turned on — not to fight them, not to bypass them, not to ignore them.