Safeguard
Open Source

prism-react-renderer: Safe Syntax Highlighting in React

prism-react-renderer gives you tokenized syntax highlighting in React without dangerouslySetInnerHTML. Here is how it works, why that matters for XSS, and how to keep the dependency healthy.

Aisha Rahman
Security Analyst
5 min read

prism-react-renderer is a React library that tokenizes source code with Prism and hands you the tokens to render as React elements, so you get syntax highlighting without ever calling dangerouslySetInnerHTML. That design choice is the security story: because the highlighted output is built from React nodes rather than an injected HTML string, the most common way syntax highlighters introduce cross-site scripting simply does not apply. It is the highlighter that powers Docusaurus code blocks, and it is popular precisely because it stays out of the dangerous-HTML business.

Most Prism integrations work by generating an HTML string and dropping it into the DOM with dangerouslySetInnerHTML. If the code being highlighted can contain user input, and the sanitization is imperfect, that is a direct XSS vector. prism-react-renderer inverts the model: Prism produces a token stream, and you map those tokens to <span> elements yourself. React escapes text content by default, so the code text renders as text, never as markup.

How the render-props model works

The library gives you a Highlight component that does the tokenizing and calls back with the pieces you need to render:

import { Highlight, themes } from "prism-react-renderer";

function CodeBlock({ code, language }) {
  return (
    <Highlight theme={themes.nightOwl} code={code} language={language}>
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        <pre className={className} style={style}>
          {tokens.map((line, i) => (
            <div key={i} {...getLineProps({ line })}>
              {line.map((token, key) => (
                <span key={key} {...getTokenProps({ token })} />
              ))}
            </div>
          ))}
        </pre>
      )}
    </Highlight>
  );
}

Notice there is no HTML string anywhere. The code prop is treated as data, tokens come back as structured objects, and every token becomes a React <span> whose text content React escapes. Even if code is fully attacker-controlled, it renders as inert text. That is the property you want in any component that displays code submitted by users — pull requests, snippet sharing, comment fields with fenced code.

The dependency you actually inherit

prism-react-renderer bundles a version of PrismJS as its tokenizing core, and this is the part worth watching. The security of your highlighting is only as good as the Prism version underneath. PrismJS has had denial-of-service issues over the years — several tied to regular-expression backtracking (ReDoS) in specific language grammars, where a crafted input string causes a highlighting pass to hang. Those live in the grammar definitions, not in the React wrapper, so keeping the wrapper current matters chiefly because it pulls in a current Prism core.

Rather than cite a specific advisory from memory, check the live data before you pin a version. Look up the package on the GitHub advisory database and confirm the bundled Prism version against any open ReDoS reports for your input languages. If you highlight a language with a known problematic grammar and you accept untrusted input, that is the case to prioritize.

Keeping it healthy

Treat prism-react-renderer like any dependency in a security-sensitive path:

  • Pin it in your lockfile and update on a cadence rather than drifting. A highlighter update that bumps the Prism core is exactly how you pick up grammar-level ReDoS fixes.
  • Watch the transitive Prism version, not just the wrapper version. The advisory you care about usually lands against PrismJS itself. An SCA tool such as Safeguard can flag the transitive Prism dependency carrying a known advisory even when your direct dependency looks fine — the kind of indirect exposure our SCA overview is built to surface.
  • Bound the input if you highlight untrusted code. Cap the length of a snippet before highlighting; ReDoS needs a large enough input to become a real hang, and a sane length limit removes most of the risk regardless of grammar.

Why this is a good default

The broader lesson generalizes past this one package: prefer libraries that keep untrusted content as data rather than converting it to HTML. prism-react-renderer is a clean example — it never gives you a foot-gun to misuse, because it never hands you an HTML string to inject. Compare that with the manual Prism-plus-dangerouslySetInnerHTML pattern, where safety depends on every integrator sanitizing correctly every time. The Academy covers this "structured over stringified" principle across other rendering libraries where the same trade-off shows up.

FAQ

Does prism-react-renderer prevent XSS?

It removes the most common highlighting XSS vector by rendering code as React elements instead of injecting an HTML string. React escapes text content by default, so attacker-controlled code renders as inert text. You still need to sanitize any other untrusted HTML elsewhere on the page.

What is the main security risk with it, then?

The bundled PrismJS core. Prism grammars have historically had ReDoS issues where a crafted input hangs the highlighting pass. Keep the package updated so it pulls a current Prism version, and cap input length if you highlight untrusted code.

How do I know which Prism version it bundles?

Check your resolved lockfile and the package's dependency metadata, then cross-reference the version against the GitHub advisory database. Composition-analysis tooling can trace and flag the transitive Prism version automatically.

Is it still maintained?

prism-react-renderer is widely used, including as the code-block engine for Docusaurus. Verify current release activity on its GitHub repository and npm page before adopting, and pin the version you land on in your lockfile.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.