Safeguard
AppSec

An XSS Example That Explains How Cross-Site Scripting Works

A clear XSS example shows how unescaped user input becomes executable script in a victim's browser, and why output encoding and CSP are the fixes that hold.

Priya Mehta
DevSecOps Engineer
7 min read

The simplest XSS example is this: an application takes text from a user, drops it into an HTML page without escaping it, and the browser runs part of that text as JavaScript instead of displaying it as words. Cross-site scripting (XSS) has stayed on the OWASP Top 10 for two decades because that mistake is so easy to make — any place your code echoes user-controlled data into a page is a candidate. This post walks through an example of XSS conceptually, shows the three main variants, and focuses on detection and remediation. There are no working payloads aimed at real targets here; the goal is to help you recognize and close the flaw in your own code.

What does a basic XSS example look like?

A basic XSS example starts with an application that reflects input straight back into the response. Imagine a search page that greets the user with what they typed:

// Vulnerable: user input written directly into the page
const term = new URLSearchParams(location.search).get("q");
document.getElementById("results").innerHTML = "You searched for: " + term;

If a visitor loads that page normally, it works fine. The problem is that innerHTML parses whatever you assign to it as HTML. If term contains angle-bracket markup instead of plain text, the browser builds real DOM nodes from it — including a <script> element or an event handler attribute that runs code. The application never intended term to be executable, but nothing in the code drew the line between data and markup, so the browser drew it wrong.

That is the entire mechanism. The attacker's "input" is really instructions, and the application handed those instructions to the HTML parser. Every XSS variant is a version of this one confusion: content that should have been treated as inert data got treated as code.

What are the three types of XSS?

The three types of XSS differ in how the malicious content reaches the victim's browser. A reflected XSS attack example is the search case above: the input arrives in a request, bounces off the server into the immediate response, and executes only for the user who followed the crafted link. It requires tricking a specific victim into clicking, which limits blast radius but makes it a common phishing companion.

Stored XSS is more dangerous because the payload is saved — in a database, a comment field, a profile bio — and served to every visitor who views that content afterward. A single injected comment can hit thousands of users with no per-victim interaction. DOM-based XSS never involves the server reflecting anything; the vulnerability lives entirely in client-side JavaScript that reads from a source like location.hash and writes to a dangerous sink like innerHTML or eval. The innerHTML snippet above is technically a DOM-based example of an XSS attack, since the browser does all the work. Knowing which type you are dealing with tells you where the fix belongs: server-side rendering, stored-data handling, or client-side code.

How does a JavaScript XSS payload actually execute?

An XSS example in JavaScript executes because the browser cannot tell your intended text apart from markup once it enters an HTML-parsing context. Consider a stored comment rendered like this:

// Vulnerable: stored comment inserted as HTML
commentEl.innerHTML = comment.body;

When comment.body is a normal sentence, the browser shows it. When it contains an image tag with an onerror handler or an inline event attribute, the browser attaches and fires that handler. A xss attack example in javascript typically abuses exactly these sinks — innerHTML, document.write, outerHTML, insertAdjacentHTML, and the outright execution of eval or setTimeout with a string argument. The script that runs then does what any script on your page can do: read cookies not marked HttpOnly, make authenticated requests as the victim, capture keystrokes, or rewrite the page to phish credentials.

The reason a javascript xss example is so potent is trust. The browser runs the injected code with the full privileges of your origin, so from the browser's perspective it is your application misbehaving, not an outsider. That is why the defense cannot be "block bad scripts" — it has to be "never let data become code in the first place."

How do you prevent XSS?

You prevent XSS by treating all user-controlled data as inert and encoding it for the exact context where it is rendered. The single most effective rule is context-aware output encoding: when data goes into HTML text, HTML-encode it; when it goes into an attribute, attribute-encode it; when it goes into JavaScript, JavaScript-encode it. Modern frameworks do most of this for you — React escapes values placed in JSX by default, and template engines auto-escape unless you opt out — which is why the vulnerable examples above use raw innerHTML rather than a framework binding.

Prefer safe sinks over dangerous ones. Assigning to textContent instead of innerHTML renders data as text, full stop, and closes the DOM-based class entirely for that line:

// Safe: rendered as text, never parsed as HTML
document.getElementById("results").textContent = "You searched for: " + term;

Layer a Content Security Policy on top. A CSP that disallows inline scripts and restricts which origins can load scripts turns a successful injection into a blocked one, because even if markup slips through, the browser refuses to execute it. Add HttpOnly on session cookies so injected script cannot read them, and validate input on the way in as a supporting control — though encoding on the way out is the load-bearing defense. React's dangerouslySetInnerHTML and its equivalents are named to warn you; when you must render user HTML, run it through a vetted sanitizer like DOMPurify first.

How do you find XSS before attackers do?

You find XSS before attackers do by combining automated scanning with disciplined code review, since the flaw hides in the gap between where data enters and where it is rendered. Dynamic testing is well suited to reflected and stored XSS: a DAST scanner exercises your running application, submits benign marker inputs, and watches whether they come back unescaped in a way the browser would execute. Because it tests the live app, it catches injection points that only appear when several components interact.

Static review complements it. Grep your codebase for the dangerous sinks — innerHTML, document.write, eval, dangerouslySetInnerHTML — and trace each back to whether user input can reach it without encoding. In CI, wire linters that flag those sinks so a new one fails review rather than shipping. The combination matters: static analysis finds the risky patterns in code, dynamic scanning confirms which ones are actually exploitable in the running system, and neither alone gives you the full picture. Building that testing habit is covered in more depth in our security academy.

FAQ

What is the simplest way to describe XSS?

XSS is what happens when an application puts user-controlled input into a web page without separating data from markup, so the browser runs part of that input as JavaScript. The fix is to always encode data for the context it is rendered in, so the browser treats it as text rather than code.

What is the difference between reflected and stored XSS?

Reflected XSS bounces a malicious input off the server into the immediate response and only affects the user who triggered the request, usually via a crafted link. Stored XSS saves the payload — for example in a comment or profile field — and serves it to every user who later views that content, giving it a much larger blast radius.

Does using React or another framework prevent XSS?

Frameworks help a great deal because they auto-escape values placed into templates by default, which closes the most common injection points. They do not make you immune: escape hatches like dangerouslySetInnerHTML, direct DOM manipulation, and unsanitized user HTML can all reintroduce XSS, so the safe patterns still require discipline.

Can a scanner find XSS automatically?

Automated tools find a lot of it. A DAST scanner exercises the running application and detects reflected and stored XSS by observing whether inputs come back unescaped, while static analysis flags dangerous sinks in source. Using both together catches more than either alone, since one finds risky patterns and the other confirms real exploitability.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.