Cross-Site Scripting (XSS): The 2026 Attack Guide

Web Security
14 min read
Cross-Site Scripting (XSS): The 2026 Attack Guide
On this page
  1. What Is Cross-Site Scripting (XSS)?
  2. How an XSS Attack Works: The Root Cause
  3. The Three Types of XSS: Reflected, Stored, and DOM-Based
    1. Reflected XSS
    2. Stored XSS
    3. DOM-Based XSS
  4. What Attackers Do With XSS
  5. XSS Payloads and Examples
  6. How to Prevent XSS
  7. How to Test for XSS
  8. Legal and Ethical Considerations
  9. Frequently Asked Questions
  10. Your Next Steps

Cross-site scripting (XSS) is what happens when a website hands your browser someone else's JavaScript and the browser runs it as if the site wrote it. That one confusion, code where data was expected, is enough to steal a session, rewrite a page, or ride a logged-in user's privileges. The quickest way to feel how easy it is to trigger is to break a real input yourself, so open HackerDNA's XSS Playground lab and pop an alert box before you read the theory.

XSS has been in the injection family of the OWASP Top 10 for as long as the list has existed, and it is still one of the most reported web bugs in 2026. This guide covers the three types you will actually meet, what an XSS attack lets an attacker do, the payloads that prove it, and the defenses that shut it down. If you want the attack side by side with its cousin, our breakdown of XSS vs CSRF is the companion read.

TL;DR: Cross-site scripting (XSS) is a web vulnerability where an application returns unescaped user input, so an attacker's script runs in another user's browser under the site's origin. It comes in three forms: reflected (payload bounced back in a response), stored (payload saved and served to everyone), and DOM-based (client-side JavaScript writes untrusted data into the page). The core fix is context-aware output encoding, backed by a Content Security Policy and framework auto-escaping. Input filtering alone does not stop it.

What Is Cross-Site Scripting (XSS)?

Cross-site scripting is a vulnerability that lets an attacker inject client-side scripts into pages that other people view. The browser cannot tell the injected code apart from the site's own code, so it runs the attacker's JavaScript with the full trust of the site's origin: its cookies, its session, its DOM.

The name is a historical accident. The interesting part is not that scripts cross sites, it is that user input crosses the line from data into executable code. A comment field, a search box, a URL parameter, a username, anything a page later renders without escaping can carry a payload that the browser executes.

XSS maps to CWE-79, and it matters because it runs in the victim's context, not the server's. An attacker who lands XSS on a banking app is not on the server, they are inside your logged-in tab, able to do anything you can do there. That is why it consistently ranks among the highest-volume findings in bug bounty programs.

How an XSS Attack Works: The Root Cause

Every XSS bug has the same origin as SQL injection: the application mixes untrusted input into an output it later interprets. For SQLi the interpreter is the database. For XSS it is the browser, and the language is HTML plus JavaScript.

Here is the mistake in its plainest form. A page echoes a search term straight into the HTML:

<!-- Vulnerable: the query is written into the page unescaped -->
<p>You searched for: <?php echo $_GET['q']; ?></p>

Send q=shoes and the page reads "You searched for: shoes". Send q=<script>alert(1)</script> and the browser does not print those characters, it parses them as a real <script> tag and runs it. The input stopped being text and became part of the page. Nothing in the string told the browser the attacker's tag was meant to be displayed, not executed.

This is why blocklisting fails here just as it does with SQLi. Teams try to strip <script> and attackers reach for <img src=x onerror=alert(1)>, or an svg, or a JavaScript URL, or an event handler on a tag you forgot. The dangerous inputs are effectively infinite because the problem is not the payload, it is that the output was never encoded for the context it landed in.

💻
Practice this now: XSS Playground lab - inject a script into an unescaped field and watch it execute, so you understand exactly what output encoding takes away from an attacker. Browser-based, no setup.

The Three Types of XSS: Reflected, Stored, and DOM-Based

What are the types of XSS? There are three: reflected XSS, where the payload is bounced back in the immediate response; stored XSS, where the payload is saved on the server and served to every visitor; and DOM-based XSS, where client-side JavaScript writes untrusted data into the page without the server ever seeing the payload. They differ in where the input lives and who gets hit.

Reflected XSS

Reflected XSS takes input from the current request, usually a URL parameter, and writes it straight into the response. The payload is not stored anywhere, so the attack needs a delivery step: the attacker crafts a malicious link and gets a victim to click it. Think of a search page that reflects your query, or an error page that echoes a bad value.

A reflected payload rides in the URL:

https://shop.example/search?q=<script>alert(document.domain)</script>

Anyone who opens that link runs the script in their own session on shop.example. Reflected XSS is the most common variant and the one you will trip over first when testing, because any parameter echoed into a page is a candidate.

Stored XSS

Stored XSS, also called persistent XSS, is the dangerous one. The payload is saved in the application, a comment, a profile bio, a support ticket, a product review, and then served to everyone who views that content. There is no link to send. The victim just loads a normal page and the attacker's script is already there waiting.

Imagine a forum where a post body is rendered without encoding. An attacker posts a comment containing a script, and every user who opens the thread executes it. Stored XSS on a high-traffic page is close to a self-spreading incident, which is exactly how the old Samy worm tore through MySpace in 2005, adding over a million friends in under a day.

DOM-Based XSS

DOM-based XSS never touches the server response. The vulnerable code is JavaScript running in the browser that reads from a source it should not trust, such as location.hash or document.referrer, and passes it to a sink that executes markup, such as innerHTML or document.write.

// Vulnerable: hash is written into the page as HTML
document.getElementById('welcome').innerHTML =
    'Hello, ' + decodeURIComponent(location.hash.slice(1));

Load the page with #<img src=x onerror=alert(1)> and the browser builds that image element and fires the handler. Because the payload lives in the fragment after #, it is often never sent to the server at all, which means server-side filters and many WAFs never see it. As single-page apps took over, DOM XSS went from an edge case to a routine finding.

What Attackers Do With XSS

An alert(1) proves the bug, but it is not the point. Once an attacker can run JavaScript in your session, they inherit your access to the page. The realistic impact usually falls into a few buckets:

  • Session hijacking. Reading document.cookie and sending it elsewhere hands the attacker your logged-in session, unless the session cookie is marked HttpOnly. That single flag is why cookie theft is far less trivial than tutorials from a decade ago suggest.
  • Acting as the user. Even without the cookie, the script can make authenticated requests on your behalf: change your email, add an admin, transfer funds. It runs from inside your session, so the app sees legitimate actions from a legitimate user.
  • Credential capture. A script can rewrite the page to add a fake login prompt or a keystroke listener on a real one, harvesting what you type into a form that looks entirely genuine.
  • Defacement and phishing. Because the code controls the DOM, it can replace content, inject misleading messages, or redirect to an attacker-controlled page, all under the trusted domain in the address bar.

When testing real applications, the finding that gets a report taken seriously is rarely the alert box. It is the follow-up: proving the payload can perform an authenticated action, in a lab or an authorized scope, so the severity is undeniable. That is the difference between "reflected value" and "account takeover" in a bug report.

XSS Payloads and Examples

You do not need an exotic payload to find XSS. You need a probe that survives the context it lands in. Start by injecting a harmless marker and looking at where and how it comes back in the HTML source, then shape the payload to the context. A few standard proof-of-concept probes, the kind used every day in authorized testing:

  • The classic tag: <script>alert(1)</script> works when your input lands between tags in an HTML body.
  • The attribute breakout: "><svg onload=alert(1)> escapes a value that was placed inside an HTML attribute.
  • The event handler: <img src=x onerror=alert(1)> runs when <script> is filtered but other tags are not.
  • The JavaScript context: ';alert(1)// breaks out of a string that was written into an inline script block.

The lesson in that list is context. The same input is harmless in one place and executable in another, which is why the fix has to be context-aware too. Use alert(document.domain) rather than alert(1) when you demonstrate a finding, since it proves which origin the script is running under and reads clearly in a report.

For a real, curated reference of payloads to try against targets you are authorized to test, the community-maintained PayloadsAllTheThings XSS list is the one most testers keep bookmarked. Treat it as a study aid, not a spray list.

How to Prevent XSS

How do you prevent cross-site scripting? Encode output for the context it appears in, so untrusted data is always rendered as inert text rather than parsed as markup or script. Encoding is the primary defense, and a Content Security Policy plus a modern framework's auto-escaping turn it into defense in depth. The OWASP XSS Prevention Cheat Sheet lays out the full ruleset; here is what actually moves the needle.

  • Context-aware output encoding. Escape data based on where it lands. HTML-encode for element bodies (< becomes &lt;), attribute-encode inside attributes, and never place untrusted data directly into a script block or an event handler. This is the fix that ends the vulnerability.
  • Let the framework escape by default. React, Angular, and Vue encode interpolated values automatically, which is why modern apps have less XSS than the PHP era. The trap is the escape hatch: dangerouslySetInnerHTML, Angular's bypassSecurityTrust, and any raw innerHTML assignment opt you back out of safety. Audit every one.
  • Deploy a Content Security Policy. A strict CSP that blocks inline scripts and restricts sources is a strong second layer. It will not fix a bug, but it can stop a payload from executing or from calling home even when one slips through.
  • Set HttpOnly on session cookies. This keeps JavaScript from reading the cookie, which neutralizes the most direct form of session theft. Pair it with Secure and a sensible SameSite value.
  • Sanitize when you truly must render HTML. If users legitimately submit rich text, do not hand-roll a filter. Run it through a vetted library like DOMPurify, which is built and tested specifically to strip dangerous markup while keeping safe formatting.

Notice what is not at the top of that list: input validation. Filtering input is a useful supporting layer, and rejecting obviously malformed data is good hygiene, but it cannot be your XSS defense. The same value is dangerous in one output context and fine in another, so the reliable control lives at output, where you know the context. Validate on the way in, encode on the way out.

How to Test for XSS

How do you find XSS in an application? Inject a unique marker into every input, then read the raw HTML response to see where it appears and whether it was encoded. Where your marker comes back intact and unescaped, shape a payload for that exact context and confirm it executes. A practical routine:

  1. Map every input. URL parameters, form fields, headers, cookies, and anything a single-page app reads from the URL fragment. Each is a candidate source.
  2. Inject a marker, not a payload, first. Send a harmless string like xss7391 and search the response source for it. If it appears encoded, that context is defended. If it appears raw, you have a lead.
  3. Match the payload to the context. Between tags, inside an attribute, or within a script block each need a different breakout. This is where understanding the three contexts pays off.
  4. Automate the breadth. A scanner in Burp Suite or a tool like Dalfox will fuzz hundreds of parameters far faster than you can by hand. Point it only at systems you own or are authorized to test.

Doing this by hand a few times builds the instinct to spot an unescaped output in a code review, which is where prevention is cheapest. Learning to exploit XSS and learning to prevent it are the same skill seen from two sides, and running the attack in a controlled lab is the fastest way to make the defensive habit automatic.

Critical reminder: Always get explicit written authorization before testing any application for XSS. Firing payloads at a site you do not own is unauthorized access under the Computer Fraud and Abuse Act (US), the Computer Misuse Act (UK), and equivalent laws worldwide, even when your payload is a harmless alert box.

  • Test only on systems you own, on intentionally vulnerable practice targets, or within the defined scope of an authorized bug bounty or engagement.
  • Use a benign proof such as alert(document.domain) to demonstrate a finding. Do not deploy payloads that steal real users' cookies or data to prove impact.
  • If you find XSS in someone else's application, report it through their disclosure process and stop there.
  • The labs and courses linked here exist so you can practice the attack and the defense legally, on targets built for exactly that.

Frequently Asked Questions

What is cross-site scripting in simple terms?

Cross-site scripting (XSS) is a web vulnerability that lets an attacker run their own JavaScript in another user's browser. It happens when a site displays user input without encoding it, so the browser treats the input as code instead of text. The script then runs with the trust of the site, giving the attacker access to that user's session and page.

What are the three types of XSS?

Reflected XSS bounces the payload back in the immediate response, usually from a URL parameter, so it needs a victim to click a crafted link. Stored XSS saves the payload on the server and serves it to everyone who views the content. DOM-based XSS happens entirely in client-side JavaScript that writes untrusted data into the page, often without the server ever seeing the payload.

Is XSS still a threat in 2026?

Yes. Modern frameworks auto-escape output, which has cut the volume of trivial XSS, but the bug remains one of the most common findings in bug bounty programs. Escape hatches like dangerouslySetInnerHTML, raw innerHTML in single-page apps, and DOM-based sinks keep XSS alive across current stacks.

How is XSS different from SQL injection?

Both are injection bugs, but they target different interpreters. XSS injects code that runs in the victim's browser, affecting that user's session. SQL injection injects code that runs in the database, affecting server-side data. XSS is a client-side attack; SQLi is server-side. The shared root cause is mixing untrusted input into an output that gets interpreted.

Does input validation stop XSS?

Not on its own. Blocklisting characters or tags can be bypassed with alternative tags, event handlers, encodings, and JavaScript URLs. The reliable defense is context-aware output encoding, so untrusted data is always rendered as inert text. Use input validation as a supporting layer, and add a Content Security Policy for defense in depth.

Related articles:

Your Next Steps

Cross-site scripting comes down to a single boundary: the moment a site lets user input become code in someone else's browser, the attacker is inside that session. The three types, reflected, stored, and DOM-based, are just different places that input can slip across the line, and the defense is the same idea from the other direction: encode output for its context, let your framework escape by default, and add a Content Security Policy so a missed spot is not a free win. The way to make that instinct stick is to see a payload execute once, then watch encoding take it away. Start with HackerDNA's free tier, no credit card required, and pop your first script in the XSS Playground lab. When you want XSS in the context of the whole web attack surface, the XSS chapter of our Web Attacks course walks through it alongside SQL injection, CSRF, and the rest of the OWASP Top 10 in guided browser labs. Learn to fire the payload, then write the page that swallows it.

HackerDNA Team

HackerDNA Team

Written by the HackerDNA team - cybersecurity professionals building hands-on hacking labs and educational content to help you develop real-world security skills.

Meet the Team

Ready to put this into practice?

Stop reading, start hacking. Get hands-on experience with 170+ real-world cybersecurity labs.

Start Hacking Free
16,000+ Hackers 100+ Labs & Courses Free
Start Hacking Free