Script Valley
Web Security Fundamentals for Developers
Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF)Lesson 3.1

How stored XSS works and why output encoding stops it

stored XSS mechanics, reflected XSS, DOM XSS, script injection via HTML, output context, HTML entity encoding, textContent vs innerHTML

Cross-Site Scripting (XSS)

Stored XSS attack flow diagram

XSS happens when attacker-controlled data is rendered as executable HTML or JavaScript in another user's browser. The browser has no way to distinguish between your code and the injected code—they both execute in the same origin.

Three XSS Types

Stored XSS: The payload is saved to the database (comment, profile bio, username) and executes for every user who views that data. This is the highest severity because it's persistent and self-spreading.

Reflected XSS: The payload comes from the URL and is reflected directly in the response without storage. Requires the victim to visit a crafted URL.

DOM XSS: The payload never hits the server. JavaScript reads from location.hash, document.referrer, or URL and writes to innerHTML.

Why innerHTML Is Dangerous

// VULNERABLE — innerHTML parses and executes script content
document.getElementById('preview').innerHTML = userInput;

// SAFE — textContent treats input as text, never as HTML
document.getElementById('preview').textContent = userInput;

// Also safe for building elements:
const div = document.createElement('div');
div.textContent = userInput;
container.appendChild(div);

Server-Side Output Encoding

const he = require('he');

// In a template engine (non-React):
const safeBio = he.encode(user.bio);
response.send(`

${safeBio}

`); // Even if bio = '