Web Security

Cross-Site Scripting (XSS) Explained: How Injected JavaScript Steals Your Session

June 17, 2026 8 min read Haven Team

XSS has appeared on the OWASP Top 10 in some form since the list's first edition. The reason it refuses to die is simple: every web application that mixes untrusted data into a page that runs trusted code is one mistake away from handing an attacker the keys. Here is how that mistake happens, and how to actually prevent it.


Cross-site scripting is a confusing name for a simple idea. An attacker gets the browser to run JavaScript that the site's author never intended to ship. Once attacker-controlled script runs in the context of your origin, it can do anything your own code can do: read cookies, read the DOM, make authenticated requests, exfiltrate data, rewrite the page. The browser's same-origin policy — the rule that normally keeps one site from touching another's data — provides no protection, because the injected code is running as the trusted site.

The name comes from an early variant where one site's script reached into another. The class outgrew the name years ago, but it stuck. What matters today is the mechanism: untrusted input ends up being interpreted as code.

The Three Classic Flavors

XSS is usually divided into three types based on where the malicious payload lives and how it reaches the victim's browser.

Type Where the payload lives Trigger
Reflected In the request itself (URL parameter, form field), echoed straight back into the response Victim clicks a crafted link
Stored Persisted on the server (a comment, profile bio, message body) Victim simply views the page
DOM-based Never touches the server — client-side JS reads attacker-controlled input and writes it to the page Victim loads a page whose script mishandles a source like location.hash

Stored XSS is the most dangerous because it is self-spreading and requires no social engineering. The infamous 2005 Samy worm on MySpace used stored XSS to add over a million friends in under a day — each infected profile infected every visitor. DOM-based XSS is the hardest to find with server-side tooling, because the vulnerable data flow happens entirely in the browser; a server-side scanner never sees the payload.

What an Attacker Actually Does With It

The textbook demonstration is <script>alert(1)</script>, but a popup is just proof of execution. Real payloads are quieter and more useful to the attacker:

Why the padlock doesn't help

HTTPS protects data in transit. XSS executes inside the browser after decryption, as first-party code on the legitimate origin. The padlock is green, the certificate is valid, and the attacker's script is running anyway. Transport encryption and code-injection are orthogonal problems.

The Defenses That Actually Work

There is no single switch. Defending against XSS is a layered discipline, and the order of importance is well established.

1. Context-aware output encoding

The root cause is data being interpreted as code, so the primary fix is encoding data for the exact context it lands in. HTML body, HTML attribute, JavaScript string, URL, and CSS each have different escaping rules. A value safe in one context can be dangerous in another. Modern frameworks — React, Angular, Vue — encode by default when you bind text, which is why they eliminate the most common reflected and stored XSS automatically. The danger reappears the moment you reach for an escape hatch like dangerouslySetInnerHTML or Angular's bypassSecurityTrust*.

2. Sanitize HTML you must render

Sometimes you genuinely need to render user-supplied HTML — rich text, or the body of an inbound email. Encoding would destroy the formatting, so you sanitize instead: parse the HTML, strip dangerous elements and attributes (<script>, onerror=, javascript: URLs), and re-serialize. A vetted library like DOMPurify is the right tool here. Never write your own regex-based sanitizer; the parsing edge cases that browsers tolerate are a graveyard of bypasses.

3. Content Security Policy as a backstop

A strict Content Security Policy turns a successful injection into a non-event. If your policy forbids inline scripts and only allows scripts from your own origin, an injected <script> tag simply won't execute. CSP is defense-in-depth — it assumes one of your other layers failed. A nonce- or hash-based policy is the modern recommendation; the older allowlist approach is easy to misconfigure into uselessness.

4. HttpOnly cookies and sensible token storage

Flag session cookies HttpOnly so injected script cannot read them, and Secure so they never travel over plain HTTP. This does not stop XSS — the attacker can still make authenticated requests — but it raises the cost of the most common goal, session theft. Storing long-lived tokens in localStorage is the opposite choice and should be made deliberately, with eyes open to the XSS exposure.

Output encoding stops XSS from happening. CSP limits the damage when encoding fails. HttpOnly limits the damage when CSP fails. You want all three, because you will eventually make a mistake in the first one.

Why This Matters for Encrypted Apps

It is tempting to think end-to-end encryption makes web-app vulnerabilities irrelevant. It does not. In a web-based encrypted messenger or email client, the decryption happens in the browser — which means the plaintext, the decryption keys, and the user's session all live in the same JavaScript context that XSS hijacks. A single injection in an E2EE web app can read messages after they are decrypted, regardless of how strong the cryptography is. This is exactly why the guarantees of end-to-end encryption stop at the edges of a compromised client.

That is why Haven's web client treats its CSP and email-HTML sanitization as security invariants, not nice-to-haves: inbound message HTML is sanitized before it ever touches the DOM, remote images are proxied rather than loaded directly, and the script policy forbids inline and third-party code. The cryptography is only as trustworthy as the surface it decrypts onto. If you are evaluating any browser-based secure-messaging tool, ask how it handles untrusted HTML and what its CSP looks like — the answer tells you whether the encryption is real protection or marketing.

Try Haven free for 15 days

Encrypted email and chat in one app. No credit card required.

Get Started →