PL-024mediumCode

User content rendered as raw HTML

Text one user wrote goes into the page as HTML, so it can run scripts in other users' browsers.

Severity
Medium
Who can exploit it
Any user who can post content others see
Found by
PatchLabs Code: your frontend code
Lab proof
Not lab-proven: file and line in the report

What it is

React escapes text by default: {comment.body} shows <script> as harmless characters. dangerouslySetInnerHTML, element.innerHTML and Markdown libraries that output raw HTML switch that protection off. PL-024 means content someone else wrote, such as a comment, a bio, a listing description or a chat message, reaches one of those without being sanitised.

A user can then post HTML that runs JavaScript in every viewer's browser (cross-site scripting). In a Supabase app this matters more than usual: supabase-js keeps the session in localStorage by default, and any script running on your page can read it and act as the viewer, including an admin who opens the content to moderate it.

Why AI builders produce it

Asked to "support formatting" or "render Markdown", the shortest route is to convert to HTML and insert it. The name dangerouslySetInnerHTML is a warning, but the code works and test content is always friendly.

Example of vulnerable code

Simplified from the patterns we see in AI-built apps. Synthetic: not taken from any real app.

src/components/Comment.tsx (vulnerable)
// src/components/Comment.tsx
import { marked } from 'marked';

export function Comment({ body }: { body: string }) {
  // body was written by another user
  return <div dangerouslySetInnerHTML={{ __html: marked.parse(body) as string }} />;
}

// A comment like this runs in every reader's browser:
//   <img src=x onerror="alert(document.domain)">

Check it yourself

check-pl-024.sh
# Places that insert HTML
grep -rnE "dangerouslySetInnerHTML|\.innerHTML *=|insertAdjacentHTML|v-html" src

# Then, on a local or staging copy, post this as a comment and view it:
#   <img src=x onerror="alert(document.domain)">
# An alert box means user content runs as code.

The fix

src/components/Comment.tsx (fixed)
// src/components/Comment.tsx
import { marked } from 'marked';
import DOMPurify from 'dompurify';

export function Comment({ body }: { body: string }) {
  const html = DOMPurify.sanitize(marked.parse(body) as string);
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

Simpler still, skip HTML: render plain text as {body}, or use react-markdown, which builds React elements and doesn't render raw HTML unless you add rehype-raw.

Code findings come with the file, the line and a one-line fix in the report, for your engineer to make. PatchLabs never pushes to your repo.

How PatchLabs finds it

code scan · report excerpt
[SCAN]    src/components/Comment.tsx:6
          dangerouslySetInnerHTML
          with unsanitised input      LEAK
[FIX]     DOMPurify.sanitize(...)
synthetic exampleReport excerpt · read from the repo, not lab-proven

PatchLabs Code flags raw-HTML insertion points whose input isn't passed through a sanitiser. It doesn't run your app or post content. The report gives the file and line; you decide whether that input can come from another user.

When the pattern is intended

HTML you control is fine: a static string, or content from your own CMS written by staff. Sanitising it anyway costs little and protects you if that changes.

Why it’s rated medium

Medium: it needs one user to post and another to view. It becomes serious when admins view user content, because their session can do more.

Guides

Free check

Does your app have PL-024?

Paste your public repo. PatchLabs Code reads it for this and every other code finding, and the Lab rebuilds your database to test who can read what.

Free for your first appReads your public repo onlyOr run the free static RLS check

Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.