Cross-Site Scripting (XSS) Explained Simply

Cross-Site Scripting (XSS) Explained Simply

Cross-site scripting (XSS) is a vulnerability that lets an attacker run their own JavaScript inside someone else’s browser session on your site. It is consistently one of the most common web bugs and also one of the most misunderstood. This guide makes it click: the three types, how a script ends up on your page, and how output encoding and a content security policy shut it down. It is written for developers, not specialists.

Quick answer: Cross-site scripting is when untrusted input is rendered into a page as HTML or script, so the browser runs it as code. The fix is context-aware output encoding—treat all user data as text when it reaches the page—backed by a content security policy as a second layer.

What XSS is

Cross-site scripting is an injection bug, closely related to SQL injection, except the interpreter being tricked is the browser rather than the database. When your app takes user-supplied data and writes it into a page without neutralising it, the browser cannot tell your intended markup from an attacker’s and executes whatever script it finds.

The payload then runs with the full privileges of the victim’s session: it can read the page, use their cookies, make requests as them, and change what they see. Because the code runs in the user’s browser rather than on your server, defenses like a firewall never even see it.

The three types: stored, reflected, DOM

There are three flavours of cross-site scripting, and they differ mainly in how the payload reaches the victim:

TypeHow the script arrivesTypical example
StoredSaved on the server, served to every viewerA comment or profile field holding script
ReflectedBounced straight back from the requestA search term echoed into the results page
DOM-basedNever touches the server; client JS renders itA URL fragment written into the page by JavaScript

Stored XSS is usually the most serious, because one injected payload can hit every visitor. DOM-based XSS is the sneakiest, since the vulnerable code lives entirely in the browser and your server logs may show nothing at all.

A concrete example

Say a page greets users with a name taken from the URL and writes it straight into the HTML. A normal visit shows a friendly hello. If the value is markup instead of a name, the browser adds that markup to the page and runs any script inside it.

Concretely, if the app inserts the raw query value into the page, a crafted link carrying <script>...</script> in place of the name turns that greeting into attacker-controlled code. The victim only has to click a link that appears to point at your trusted domain. Nothing was stored and no server was compromised—the input was simply rendered as markup.

Why it's dangerous

Because the script runs as the logged-in user, cross-site scripting is effectively a foothold inside their session. That is more dangerous than it first sounds:

  • Session hijacking: steal or ride the user’s cookies to act as them.
  • Credential theft: draw a convincing fake login prompt over the real page.
  • Silent actions: send requests on the user’s behalf—change an email, transfer, or post.
  • Defacement: rewrite the page or redirect the victim to malware.

An administrator who merely views an attacker’s stored payload can hand over control of the whole application without noticing anything happened.

The fix: output encoding

The core fix for cross-site scripting is output encoding: when you place user data into a page, encode it for the exact context so the browser reads it as text, not code. The same value is encoded differently depending on where it lands.

  • In an HTML body, turn < into &lt; and > into &gt; so tags render as characters.
  • In an HTML attribute, quote the value and attribute-encode it.
  • In JavaScript or a URL, use the encoding built for that context—they are not interchangeable.

The golden rule is to keep data as data. Never build HTML by concatenating user input, and avoid sinks like innerHTML or document.write that parse strings as markup—prefer text APIs such as textContent.

Content Security Policy

A content security policy (CSP) is an HTTP header that tells the browser which sources of script it is allowed to run. Even if a payload slips past your encoding, a strict CSP can stop it from executing, which is why it belongs as a second layer rather than the first.

A good policy blocks inline script and only allows code from sources you trust, often using per-request nonces or hashes. Start in report-only mode to find what breaks, then enforce. CSP is powerful but fiddly, and it protects best when it is genuinely strict; a policy full of wildcards offers little. We cover the header itself in the security headers guide.

Framework protections and their gaps

Modern frameworks are the biggest reason XSS is less common than it once was. React, Angular, Vue, and most server templating engines auto-encode values by default, so ordinary interpolation is safe out of the box.

The gaps are the escape hatches. dangerouslySetInnerHTML in React, v-html in Vue, the bypass-security methods in Angular, and the raw or “safe” filters in many templates all tell the framework to stop encoding—and that is exactly where cross-site scripting creeps back in. If you must render user-supplied HTML, sanitise it first with a vetted library instead of trusting the input.

Frequently asked questions

What is the difference between XSS and SQL injection?

Both are injection bugs where input is treated as code, but the target differs. SQL injection tricks the database into running attacker SQL; cross-site scripting tricks the browser into running attacker JavaScript. The mindset—keep data and code separate—is identical.

Does a content security policy replace output encoding?

No. Output encoding is the primary fix because it stops the payload from ever becoming script. A CSP is a valuable safety net for the cases you miss, but a policy alone will not save a page that injects raw user HTML.

Are single-page apps and React immune to XSS?

No, though they are safer by default because they auto-encode. The risk returns whenever you use an escape hatch like dangerouslySetInnerHTML or render HTML received from users or an API. Sanitise any HTML you must inject.

Is XSS only a problem for logged-in users?

No. It is worse for authenticated sessions because the script can act as the user, but XSS can also phish credentials, deface pages, or redirect any visitor to malware. Anonymous pages that reflect input are still worth fixing.

Cross-site scripting sounds exotic, but the defense is a habit: treat everything a user sends as text, encode it for the context where it lands, and lean on your framework instead of hand-building HTML. Add a strict content security policy and you have a strong, layered defense. For the big picture, start with our cornerstone guide.

Last updated: July 6, 2026

Comments

Popular posts from this blog

The OWASP Top 10 Explained for Developers