What is XSS? Can user input really run scripts in my app?
In one sentence:Cross-site scripting (XSS) is a security vulnerability where an app renders untrusted input as executable code instead of plain text, letting an attacker's JavaScript run in other users' browsers with the same access as the app itself.
What it actually is
Cross-site scripting (XSS) is what happens when your app takes something a person typed, a comment, a username, a search box, a link parameter, and puts it into the page as code instead of as words. OWASP classifies it as a type of injection, grouped with SQL injection under A03:2021-Injection and tracked as CWE-79, because the underlying mistake is the same: data from an untrusted source gets treated as instructions.
Think of the difference between quoting someone and following their orders. When your app writes what a user typed onto the page as plain text, it’s quoting them. Their words show up, but nobody acts on them. When it writes that same text into the page as raw HTML without checking it first, it’s following their orders instead. Any script tag or event handler they slipped in runs exactly as if you had written it yourself, in every browser that loads the page, with access to whatever your app can touch: cookies, local storage, the logged-in session, everything else on that page.
This comes in three shapes. Stored XSS sits in your database and runs for every visitor who loads the poisoned comment or bio. Reflected XSS bounces back in a single response, delivered through a crafted link someone has to click. DOM-based XSS never touches your server at all: client-side JavaScript reads something like the page’s URL and writes it straight into the page. The same trust mistake resurfaces inside AI agents as prompt injection: text meant to be data, treated as instructions instead.
Why your AI just did this
React escapes text by default. Write <div>{comment}</div> in JSX, and React converts any <, >, or & in comment into safe entities before it reaches the DOM. Type <script>alert(1)</script> into a comment box, and the page shows those literal characters as text, not a running script.
The hole opens when your agent needs to render actual HTML instead of escaped text: a rich-text bio, a markdown preview, an email template. To show real formatting, React needs dangerouslySetInnerHTML, which works like setting an element’s raw innerHTML directly: both bypass escaping entirely and execute anything embedded in the string, including an <img src=x onerror="..."> tag that never needs a <script> at all. Lovable, Cursor, and Claude Code reach for one of these the moment you ask for “let people format their bio” or “preview the markdown,” because it’s the most direct way to satisfy that request. The protection holds only until your own code opts out of it.
When you’ll run into it
There’s rarely an error message, which is what makes this harder to spot than something breaking outright. If it works, it works silently: the script just runs, no red text in the console, nothing crashing. The first sign is often you testing your own feature. Paste <b>hello</b> into a bio field, and if the page shows an actual bold word instead of the literal tag, that field renders HTML, which means anything else typed there runs too.
You’ll meet it in comment sections, bio or profile fields, markdown or rich-text previews, and admin panels that display user-submitted names or messages. It also shows up as a warning from a dependency scanner, or from your own agent flagging a dangerouslySetInnerHTML call it just wrote as something worth a second look before you ship it.
What to check
- Search your codebase for
dangerouslySetInnerHTMLand rawinnerHTMLassignments. Each one is a spot where escaping was turned off on purpose, and each deserves a reason - Anywhere you render user-submitted HTML (rich text, markdown output, pasted content), sanitize it with a library like DOMPurify before it reaches the page, rather than trusting where the content came from
- Keep sanitizing rich text separate in your head from ordinary input validation: validation checks that data has the right shape, sanitization strips out anything that could execute
- Never build HTML by pasting user input into a template string and writing the result straight into the page; let the framework’s default text rendering handle it
- Test any free-text field by typing
<script>alert(1)</script>or<img src=x onerror=alert(1)>into it. A popup means that field is unsanitized - Add a Content Security Policy header as a backup layer, not the fix itself. A strict
script-srclimits what a script can do even if one slips through
Say it like a dev
Instead of: “someone said user input can run scripts in my app, is that true?” Say: “Search the codebase for dangerouslySetInnerHTML and innerHTML. Anywhere user-submitted text reaches one of those, sanitize it with DOMPurify before rendering.”
Instead of: “make the bio field support bold text and links” Say: “Render the bio as sanitized HTML with DOMPurify, not raw dangerouslySetInnerHTML. Only allow a small set of safe tags.”
Instead of: “the comment section feels insecure” Say: “Add a Content Security Policy header restricting script-src, as a backup in case unescaped user input slips through somewhere.”
People actually ask
“Someone said user input can run scripts in my app. Is that actually my app's fault?”
Yes, if your app takes something a user typed and renders it as HTML instead of plain text, whatever they typed can execute in every browser that loads that page. This is called cross-site scripting (XSS), and OWASP counts it as a type of injection vulnerability. Frameworks like React escape plain text by default, so the risk usually traces back to a specific spot in the code that deliberately turned that off, like dangerouslySetInnerHTML.
“What's the difference between XSS and SQL injection?”
Both trick your app into treating a user's data as instructions, they just aim at different targets. SQL injection sneaks commands into a database query, while XSS gets a script to run inside your app's page for whoever views it. OWASP groups them under the same injection category because the root cause is identical: unescaped input reaching a place that executes it.
“How do I actually fix an XSS vulnerability?”
Start by not rendering raw HTML from user input where you can avoid it, since frameworks already handle plain text safely. If you do need to show formatted content, like a rich-text bio, sanitize it with a library such as DOMPurify before it reaches the page, and add a Content Security Policy header as a second layer in case something still slips through.
Related terms
Checked against
Just met this in a real session? Glim reads it in plain words.
Paste what your agent just did, a git diff, your terminal, or its summary, and Glim tells you what changed and what to check before you ship. Nothing stored.
Explain my session →