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
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
# 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
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
[SCAN] src/components/Comment.tsx:6 dangerouslySetInnerHTML with unsanitised input LEAK [FIX] DOMPurify.sanitize(...)
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.
Related findings
Admin check only in the browser
The admin page is hidden in the app, but nothing stops a normal user asking the database for the same data.
PL-023Dependency with a known vulnerability
A package version in your lockfile matches a published advisory in the OSV database.
PL-017Edge function open without sign-in while using the service key
A server function holding the key that ignores every rule, with nothing checking who is calling it.
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.
Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.