---
title: "Cross-site scripting (XSS)"
slug: xss
severity: high
category: Injection
summary: "User content rendered as HTML runs in your origin. It can read that user's data and call your API as them."
platforms:
  - Next.js
  - Supabase
  - Vercel
updated: "2026-10-13"
related:
  - unsafe-file-uploads
  - missing-security-headers
  - prompt-injection
---

React escapes text you put in JSX. That default is why a comment rendered as `{comment.body}` is usually safe. The bugs are the escapes from that default: `dangerouslySetInnerHTML`, a markdown renderer that emits raw HTML, setting `innerHTML`, or an `href` that starts with `javascript:`.

Stored XSS is the version that matters for a small app. One user saves a display name or a bio. Every other user who views it runs the script. The script is same-origin, so it can call your APIs with the victim's cookies.

A content security policy reduces how useful a bug is. It does not make `dangerouslySetInnerHTML` of user HTML acceptable. See [missing security headers](/issues/missing-security-headers).

Model output is untrusted text too. If you render an LLM reply as HTML, you have the same bug, plus [prompt injection](/issues/prompt-injection).

## How to check in 5 minutes

1. Search for `dangerouslySetInnerHTML`, `innerHTML`, `markdown`, and `rehype-raw`. Open each and see whether the HTML can include a user's string or a model's reply.
2. Search links built from user input. An `<a href={user.url}>` is safe for `https` and dangerous for `javascript:`.
3. On a staging account you own, save `<img src=x onerror=alert(1)>` or a markdown image with an error handler as a profile field. View it as another user. If you see the dialog, or the image request fires, the field is rendered as HTML.
4. Check email templates and PDF rendering if they interpolate names into HTML the same way.

## How to fix

### Next.js

Render user text as text. If you need formatting, use a markdown renderer that does not pass raw HTML through, or sanitize with a maintained HTML sanitizer and a strict allowlist (no `script`, no event handlers, no `javascript:` URLs). DOMPurify is a widely used sanitizer. Pin the version and keep it updated.

```tsx
export function Bio({ bio }: { bio: string }) {
  return <p>{bio}</p>;
}
```

For links, allow only `https:` after parsing the URL. Reject everything else.

Do not build a sanitizer with a regex. You will miss a browser quirk.

### Supabase

The database can store the characters. That is fine. The bug is rendering them as HTML. You do not need to strip `<` on the way in if every view escapes on the way out. Do not reflect a stored error string into an admin page with `dangerouslySetInnerHTML` either.

### Vercel

A header such as `Content-Security-Policy` on the deployment limits script sources. Set it in `next.config` or `vercel.json`. A CSP is a second layer. Remove the HTML sink first. See [missing security headers](/issues/missing-security-headers).

### Firebase

Same rendering rules. Security rules do not stop XSS. They stop the write, not the script in a field you chose to render.

## Agent prompt

```text
You are removing cross-site scripting sinks. Do not add features.

Search for dangerouslySetInnerHTML, innerHTML, and markdown or HTML renderers.

For each:
1. If the input can include user or model text, render it as text, or sanitize HTML with a maintained allowlist sanitizer. Do not write a regex sanitizer.
2. Disallow javascript: and data: URLs in links built from user input. Allow https only, unless I named another scheme.
3. Do not turn off React's escaping to make formatting work.

Show each sink and what you did with it.
```

## FAQ

### React escapes everything, so can I ignore this?

React escapes JSX text. It does not escape HTML you insert on purpose, HTML you generate from markdown, or URLs in `href`.

### Should I strip angle brackets when saving to the database?

Escaping at render is the reliable fix, because a new screen can forget a strip-on-write rule. Stripping on write also corrupts legitimate text like "1 < 2".

## Sources

- [OWASP Cross Site Scripting Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
