On this page
  1. What is cross-site scripting?
  2. What are the three types of XSS?
  3. How does a stored XSS attack work?
  4. How do I prevent XSS?
  5. Why does AI-generated code still have XSS?
  6. Checklist
  7. FAQ
    1. What is XSS in simple terms?
    2. Is XSS still a problem in 2026?
    3. What is the difference between stored and reflected XSS?
    4. Does React prevent XSS?
    5. How do I fix an XSS vulnerability?
    6. Can XSS steal my session cookie?
  8. Related topics
  9. Sources
concept

What Is Cross-Site Scripting (XSS)?

XSS lets attacker-controlled JavaScript run in another user's browser. Learn the three types, why AI-generated React still ships it, and how to prevent it.

Quick answer

  • XSS is a vulnerability that lets attacker-controlled JavaScript execute in another user’s browser, on your site’s origin.
  • The three types are stored (saved in your database), reflected (delivered via a crafted link), and DOM-based (introduced entirely in client-side JavaScript).
  • The fix is output encoding plus sanitization, backed by a Content-Security-Policy as a second layer.
  • React escapes JSX by default, so AI-generated React is usually safe — until it reaches for dangerouslySetInnerHTML, innerHTML, or an unsanitized Markdown renderer.

What is cross-site scripting?

Cross-site scripting (XSS) is a web vulnerability where an attacker gets their own JavaScript to run inside another visitor’s browser while they are using your site. The code is not written to your server by the attacker; it is written into a page your server sends, and the browser runs it as though your own frontend wrote it.

The danger comes from where it runs. Because the script executes on your origin, it inherits everything that origin can do: read the DOM, read document.cookie unless the cookie is HttpOnly, read tokens in localStorage, and issue authenticated requests. The victim never sees anything wrong, because the attack usually happens invisibly.

Where this bites vibecoders

The specific way this bites vibecoders is Markdown and HTML rendering. The moment a feature says “let users write rich text” or “render the README/description as HTML,” the code has a path that emits raw markup, and AI assistants reach for dangerouslySetInnerHTML, innerHTML, or a Markdown library with an option literally named dangerously — because the feature does not work without it. The app looks perfect and functions perfectly. The vulnerability is invisible until someone pastes a <script> tag into a bio field.

What are the three types of XSS?

The three types differ only in where the malicious payload travels, not in what it does once it runs.

TypeWhere the payload livesWhat the victim doesSeverity
Stored (persistent)Saved in your databaseViews the page — no link neededHighest: hits every viewer, including crawlers and other users’ sessions
Reflected (non-persistent)In a URL parameter or requestClicks a crafted linkHigh when the app is a link distributor (email, forums)
DOM-basedNever leaves the browser; assembled in client-side JSLoads a page that reads a controllable valueHigh, and hardest to find because the server never sees the payload

All three end the same way: alert(document.cookie) or a fetch call carrying the victim’s credentials. Only the delivery mechanism differs.

How does a stored XSS attack work?

A stored XSS attack works by saving a script inside data your app later renders back to other users. The classic example is a comment, a profile bio, or a product review.

The attacker submits text containing markup. Your server stores it verbatim because, from its point of view, it is just a string. Later, another user opens the page, your server sends the stored text inside the HTML, and the browser parses it as markup rather than text. The embedded script runs with that user’s session.

The failure is a decision made at write time and exploited at read time: the server trusted that stored content was safe to emit as HTML. The durable fix is to escape on output, not on input, because escaping on input breaks as soon as a second rendering context appears (an email notification, an admin panel, an API consumer).

How do I prevent XSS?

Four defenses, in order of how much they matter.

1. Encode output for its context. Output encoding converts <, >, &, ", and ' into their HTML entities so the browser renders them as characters instead of markup. “Context” matters: inside an HTML body, inside an attribute, inside a JavaScript string, and inside a URL each need different encoding. This is the primary defense and the one frameworks implement for you.

2. Sanitize when you deliberately allow HTML. If the feature genuinely needs rich text, run the value through a vetted sanitizer (DOMPurify in the browser, a maintained server-side equivalent) that strips scripts, event handlers, and dangerous URL schemes while keeping formatting. Sanitizing is not the same as encoding — pick one deliberately.

3. Deploy a Content-Security-Policy. A CSP tells the browser which script sources are allowed, so even if an injection lands, the payload is refused. It is a genuine second layer, not a replacement — see What Is Content Security Policy (CSP)?.

4. Use HttpOnly cookies. This does not stop XSS, but it removes the most valuable prize: the script can no longer read the session cookie directly. Pair it with SameSite to blunt CSRF, which is a different bug with a similar neighborhood — see What Is CSRF (Cross-Site Request Forgery)?.

Why does AI-generated code still have XSS?

Frameworks make the safe path the default, so the base output is usually fine. The failures are concentrated in the escape hatches, and they are consistent enough to grep for.

  • dangerouslySetInnerHTML used for convenience. The name warns the developer; the assistant still reaches for it whenever asked to render formatted content, and usually does not sanitize the input.
  • Markdown/HTML renderers with raw HTML enabled. Libraries render user content to HTML and are frequently configured with an option that permits raw tags, so a stored payload survives straight through.
  • HTML assembled with string concatenation. Building a fragment like `<div>${userInput}</div>` and assigning it to innerHTML reintroduces the exact bug the framework was preventing.
  • Server-side templates with a | safe or dangerouslySetInnerHTML equivalent. Jinja’s | safe, Handlebars’ triple-stash {{{ }}}, and Twig’s | raw all disable escaping for that value.
  • URLs built by concatenation. A href of javascript:... is an execution sink that plain HTML escaping does not catch.
  • No CSP at all. The assistant rarely adds a policy unless asked, so nothing backstops the escaping layer.

Checklist

  • Keep the framework’s default escaping on; do not disable it globally to make something render.
  • Search the codebase for dangerouslySetInnerHTML, innerHTML, | safe, | raw, and {{{.
  • Sanitize every value that is intentionally rendered as HTML, and treat the sanitizer as a dependency you pin and update.
  • Encode for the specific output context — HTML body, attribute, JavaScript string, and URL are not interchangeable.
  • Set a Content-Security-Policy that does not include unsafe-inline.
  • Mark session cookies HttpOnly and SameSite=Lax or stricter.
  • Never build URLs or HTML by concatenating user input.
  • Treat any Markdown or rich-text field as attacker-controlled, including ones only admins can edit.

FAQ

What is XSS in simple terms?

Cross-site scripting is a bug that lets attacker-written JavaScript run inside your web page when a real user visits it. The code executes in the user’s browser, on your site’s origin, which means it can read the page, read anything stored in cookies or local storage, and make requests as that logged-in user.

Is XSS still a problem in 2026?

Yes. Modern frameworks escape output by default, which removed a large class of bugs, but the escape hatches are still widely used: rendering Markdown or HTML from a database, using innerHTML or dangerouslySetInnerHTML, and building HTML by concatenating strings. OWASP continues to rank injection, including XSS, in its Top 10 — see What Is the OWASP Top 10?.

What is the difference between stored and reflected XSS?

Reflected XSS travels in a URL or request and runs when a victim clicks a crafted link. Stored XSS is saved in your database first and runs for every user who views the page that contains it, which makes it far more damaging because no victim interaction is required beyond opening the page.

Does React prevent XSS?

React escapes text written into JSX by default, so ordinary interpolated values are safe. React stops protecting you the moment you use dangerouslySetInnerHTML, set innerHTML directly, build an HTML string yourself, or pass content through a Markdown renderer that emits raw HTML. Those are the places XSS actually lands in React apps.

How do I fix an XSS vulnerability?

Encode output for the context it lands in, sanitize any HTML you deliberately allow, and set a Content-Security-Policy as a second layer. Prefer frameworks that escape by default over ones that do not, and never assemble HTML with string concatenation of user input.

Not if the cookie is HttpOnly — the browser refuses to expose it to JavaScript, so injected script cannot read it. Cookies without that flag are readable by any script on the page, which is why HttpOnly is a near-universal recommendation. The attack still succeeds against the session through requests the browser attaches automatically, which is why encoding remains the real fix.

Sources

Share: