106

I would like to have a dynamic blog on my site (which uses React). Initially, I was going to store the posts in raw HTML in my database and generate the content using dangerouslySetInnerHTML. I am however concerned about the security implications. While my app doesn't have any sensitive data, I'm not well enough versed in XSS to know all the dangers I'd be opening my app up to.

I'm curious if there's a performant, safe way to dynamically load blog pages within my app. Would using https://github.com/odysseyscience/react-router-proxy-loader be useful in this case? Have a folder of blog post JSX separate from the rest of my app and load it using this (admittedly, I'm not sure how react-router-proxy-loader works).

I'm open to suggestions.

Jon Schneider
  • 25,758
  • 23
  • 142
  • 170
Kevin
  • 1,403
  • 2
  • 11
  • 15
  • 5
    Right? `dangerouslySetInnerHTML` implies that there must be some alternative "best practice" way of doing this, but I haven't found a way to store content (paragraphs, etc.), that doesn't require `dangerouslySetInnerHTML`...but I just started today. I have a feeling tho, that the name is merely to remind you to stay aware of what you are doing, and that it's safe otherwise. – WraithKenny Jun 24 '15 at 20:01
  • To paraphrase the previous comment: "I haven't found an easy alternative, so I'll assume it's not dangerous even though it's literally in the name." – inwerpsel May 09 '22 at 14:32
  • https://pragmaticwebsecurity.com/articles/spasecurity/react-xss-part2.html – inwerpsel May 09 '22 at 14:36

9 Answers9

89

If XSS is your primary concern, you can use DOMPurify to sanitize your HTML before inserting it in the DOM via dangerouslySetInnerHTML. It's just 10K minified. And it works in Node too.

mik01aj
  • 11,928
  • 15
  • 76
  • 119
Alexandre Kirszenberg
  • 35,938
  • 10
  • 88
  • 72
31

The article How to prevent XSS attacks when using dangerouslySetInnerHTML in React suggests to use jam3/no-sanitizer-with-danger eslint rule to check that the content passed to dangerouslySetInnerHTML is wrapped in this sanitizer function

Example of valid code is

const sanitizer = dompurify.sanitize;
return <div dangerouslySetInnerHTML={{__html: sanitizer(title)}} />; // Good

It also describes 3 sanitizer libraries:
DOMPurify
Xss.
xss-filters.

Michael Freidgeim
  • 26,542
  • 16
  • 152
  • 170
12

https://facebook.github.io/react/tips/dangerously-set-inner-html.html

"The prop name dangerouslySetInnerHTML is intentionally chosen to be frightening. ..."

"After fully understanding the security ramifications and properly sanitizing the data..."

I figure, if you trust your own CMS/Server (and not receiving from a 3rd party), which has sanitized the data (this step is also done), then you can insert using dangerouslySetInnerHTML.

As Morhaus said, maybe use DOMPurify as it (bonus) probably handles this unfortunate bit: "so the HTML provided must be well-formed (ie., pass XML validation)." I suspect some content using the non-XML version of HTML5 might otherwise be an issue. (Note: I haven't used it myself yet, since i'm new like you.)

WraithKenny
  • 1,001
  • 13
  • 15
  • Considering the React docs are now updated; heres the link to the new docs: https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html. – Jeremy Trpka Apr 12 '23 at 19:47
9

As stated in other answers, a lot of libraries (dompurify, xss, etc) can parse the HTML you are giving to the browser, remove any malicious part and display it safely.

The issue is: how do you enforce these libraries are used.

To do so, you can install RisXSS which is an ESLint plugin that will warn the uses of dangerouslySetInnerHTML if you do not sanitize it before (in a sense, this is an improved version of react/no-danger ESLint rule).

To do so, install dompurify and eslint-plugin-risxss:

npm install dompurify eslint-plugin-risxss

Add risxss to your ESLint plugins then use DOMPurify:

import { sanitize } from 'dompurify';

export const MyArticle = ({ post }) => (
  <>
    <div dangerouslySetInnerHTML={{ post.content }} /> {/* You will receive a warning */}
    <div dangerouslySetInnerHTML={{ __html: sanitize(post.content) }} /> {/* All good here */}
  </>
);

Disclaimer: I am a contributor of RisXSS plugin.

clementescolano
  • 485
  • 5
  • 15
6

If you're sure the input HTML is safe (without XSS risk) but might be malformed (e.g. have a random < in text), and you want to prevent your app from failing because of unexpected DOM change, then you could try this:

function sanitize(html) {
  var doc = document.createElement('div');
  doc.innerHTML = html;
  return doc.innerHTML;
}

(Based on https://stackoverflow.com/a/14216406/115493)

But if you have the slightest doubt that your HTML might be not-XSS-safe, use DOMPurify as mentioned above.

Community
  • 1
  • 1
mik01aj
  • 11,928
  • 15
  • 76
  • 119
1

Use React library Interweave.

  1. Safely render HTML without using dangerouslySetInnerHTML.
  2. Safely strip HTML tags.
  3. Automatic XSS and injection protection.
  4. Clean HTML attributes using filters.
  5. Interpolate components using matchers.
  6. Autolink URLs, IPs, emails, and hashtags.
  7. Render Emoji and emoticon characters.

https://www.npmjs.com/package/interweave

SaimumIslam27
  • 971
  • 1
  • 8
  • 14
1

Safe alternative to dangerouslySetInnerHTML

I'm curious if there's a performant, safe way to dynamically load blog pages within my app

It's interesting that, while the question is for an alternative to dangerouselySetInnerHTML, all of the answer instruct to use it anyway. Does this mean you're stuck with no alternative? No, just no easy one.

Alternative 1: Rewrite your app to pass data to components instead

Initially, I was going to store the posts in raw HTML

It doesn't seem from the answer like this is a requirement. It might be implemented as an easy solution, to not worry about the data schema matching your components. But it's unlikely this is a hard requirement.

It should be possible to re-architect your app to not rely on storing raw HTML, though it might mean a lot of work, depending on the app.

Alternative 2: Don't write your own page builder

If you're just interested in having a blog on your site, it's maybe not worth it to write your own page builder. Even if you want tight integration with other parts of your site, that's still possible if you use something like WordPress.

Alternative 3 (WordPress only): Use InnerBlocks

If you're currently using dangerouselySetInnerHTML in a WordPress block, you can convert it to use InnerBlocks instead.

inwerpsel
  • 2,677
  • 1
  • 14
  • 21
0

I'm faced the same issue and ended with better solution. if your input something like below and solution will work for you using lodash

&lt;em&gt;paragraph text example:&lt;/em&gt;

My Solution:

import _ from 'lodash';

const createMarkup = encodedHtml => ({
  __html: _.unescape(encodedHtml),
});

/* eslint-disable react/no-danger */
const Notes = ({ label }) => (
  <div>
    <div dangerouslySetInnerHTML={createMarkup(label)} />
  </div>
);
Venkat.R
  • 7,420
  • 5
  • 42
  • 63
  • 2
    This solution is still vulnerable to injection. Please use one of the other answers that uses a sanitizer library. – Chris Groh Jul 26 '19 at 19:40
  • @ChrisGroh, agree to your point :+1 need to be careful and make sure the HTML string is coming from a trusted source else it cause security issue. – Venkat.R Jan 26 '20 at 15:02
-1

you should add if condition when you use dompurify (this case is in frontend project) this is a simple code

import DOMPurify from 'dompurify'
if (typeof window !== `undefined`) {
    const domPurify = DOMPurify(window);
    title = domPurify.sanitize(title);
}
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Cornel Raiu Sep 26 '22 at 07:55