Lessons · LLM10: Improper Output Handling · error

Model markdown is rendered with raw HTML on

HTML inside a model reply goes into the page as live HTML, so it can run scripts.

Caught by model-output-rendered-as-markdown-without-sanitization · CWE-79, CWE-80

What is wrong

Markdown renderers escape HTML by default. If a reply contains <script>, it is shown as those characters instead of being run. That escaping is the protection.

This code turns the protection off: rehype-raw with no sanitizer, allowDangerousHtml, marked with sanitize: false, or markdown-it with html: true. Whatever the reply contains becomes live HTML in the visitor's browser.

Model replies count as untrusted text. A model can be asked to include HTML, and it will.

How it is used against you

Your chat UI renders the model's markdown with raw HTML turned on, because tables looked broken without it.

<ReactMarkdown rehypePlugins={[rehypeRaw]}>
  {message.text}
</ReactMarkdown>
  1. An attacker shares a chat link, or plants text the model will repeat, containing an image tag with an onerror handler.
  2. The model includes that tag in its reply.
  3. Your page renders it as real HTML. The image fails to load, so the onerror code runs.
  4. The script runs as your site, in the victim's browser. It can read what the page can read and act as the signed-in user.

The cost: Account takeover for anyone who views the message, and the attacker can do anything the user could do in your app.

How to spot it

  • rehype-raw without rehype-sanitize after it.
  • allowDangerousHtml, marked with sanitize: false, or markdown-it with html: true.
  • Your AI builder said "to render the formatting, enable HTML".

The fix

Paste this into Claude Code, Cursor, or whatever wrote the code. When llm-audit finds the mistake in your project, npx llm-audit prompt gives you the same prompt with every file and line filled in.

Fix this in my project: model output is rendered as markdown with raw HTML allowed and no sanitizer.

Please:
1. Turn raw HTML off in the markdown renderer.
2. If some HTML is truly needed, add rehype-sanitize after rehype-raw, with an allowlist of only the tags I use.
3. Do not use dangerouslySetInnerHTML or innerHTML for anything that came from a user or a model.

Show me the diff and tell me which tags the allowlist keeps, if any.

Check the rest

A prompt that looks for the same mistake everywhere else, without changing anything yet.

Check this project for places that put untrusted text into a page as raw HTML, before I ship it.

Look for:
- dangerouslySetInnerHTML, innerHTML, outerHTML, insertAdjacentHTML, or document.write with anything that is not a fixed string.
- Markdown rendering with raw HTML turned on (rehype-raw without rehype-sanitize, allowDangerousHtml, markdown-it with html: true).
- Server responses built from request data with res.send or res.write.

For each one, give me the file and line, say where the text comes from (user, database, or AI model), and show a payload that would run a script. Do not change any code yet.

Find it in your code

npx llm-audit scan

Runs on your machine with Semgrep. Nothing is uploaded.