XSS (Cross-Site Scripting)
How text a user writes turns into JavaScript that runs in other people's browsers, what that lets an attacker do, and how encoding output prevents it.
- Track
- Web security
- Estimated time
- 30 min
- Level
- Beginner
Before this lesson
XSS happens when a site shows text a user wrote without treating it as text, so the text turns into JavaScript that runs in the browser of everyone who opens the page. The code then runs as the victim: it can read their session, act on their behalf, or change what they see. Sites display user content everywhere — comments, names, search results — which is why XSS is one of the most common web vulnerabilities.
What is the vulnerability?
A page is just HTML text. The browser reads that text and decides what is a heading, what is a link, and what is a <script> to run. If an application builds its HTML by pasting in what a user typed, the user can type not text but markup:
const q = req.query.q;
res.send('<p>No posts match: ' + q + '</p>');Search for crow and the page is <p>No posts match: crow</p>. But search for <script>steal()</script> and the page becomes:
<p>No posts match: <script>steal()</script></p>The browser has no way to know the script was not meant to be there. It runs it.
Why does it happen?
The browser cannot tell the difference between markup the developer wrote and markup that arrived inside user input — it is all one stream of text. The only thing that keeps < as a less-than sign instead of the start of a tag is Turning characters that have meaning in HTML into harmless entities: < becomes <, > becomes >, and so on.. When output is not encoded for the place it is going, user text becomes code.
The three kinds
- Reflected: the input comes in on a request and bounces straight back in that same response (a search term in the results page). The attacker sends the victim a crafted link.
- Stored: the input is saved and shown to everyone who later opens the page (a comment, a profile name). One payload hits many victims.
- DOM-based: the server is not involved at all. The page’s own JavaScript reads something the attacker controls (the part of the URL after
#) and writes it into the page withinnerHTML.
What it lets an attacker do
Code that runs in the victim’s page runs with all of the victim’s access. It can read document.cookie and send the session to the attacker, make requests as the victim (post, transfer, change a password), log keystrokes, or rewrite the page to phish. Stored XSS on a busy page can hit every visitor at once.
How to prevent it
Encode output for its context, and prefer APIs that cannot produce markup:
// 1. Encode when you build HTML from data.
res.send('<p>No posts match: ' + escapeHtml(q) + '</p>');
// 2. Better: never build HTML from strings. Set text as data.
element.textContent = q; // not innerHTML
img.setAttribute('alt', q); // not '<img alt="' + q + '">'
// 3. If you must allow some HTML, whitelist-sanitize with a mature library.
element.innerHTML = DOMPurify.sanitize(q, { ALLOWED_TAGS: ['b', 'i', 'a'] });The rule is: encode on output, not on input, with the right encoding for each context (HTML text, HTML attribute, JavaScript, URL). Modern template engines do this by default — the danger is the one place you reach for raw innerHTML or string concatenation. A strong Content-Security-Policy is a valuable second layer, but it is a safety net, not the fix.
Labs
A search that echoes you back
SolvedEasyBrowser lab
Make the results page run code that calls steal() — the code that, in a real attack, would send the victim’s session to the Ravens.
Enter the labLockedA comment that waits for the next visitor
SolvedMediumBrowser lab
Leave a stored comment so that when the leader opens the thread, his browser runs code that calls steal().
Enter the labLockedA link that breaks itself in the browser
SolvedHardBrowser lab
Craft a URL fragment (after the #) that makes the victim’s browser run code calling steal(). Note: no request is sent to the server.
Enter the labLocked
