React: Preventing XSS Attacks

Intro

We are currently building a little widget that uses a search box to query the Wikipedia API and show the results on the screen.

results on screen

We can see that our results are coming back with some HTML that is being displayed as a string. This is because we are just dumping the Wikipedia results on the page without manipulating them in any way, and React actually sanitizes any content coming from an outside source by default. Unlike in Express where we have to sanitize things manually. We can peek at our code here and see that we are just dumping it in our JSX without manipulating it in any way.

components/Search.js
import React, { useState, useEffect } from "react";
import axios from "axios";

const Search = () => {
  const [term, setTerm] = useState("React");
  const [results, setResults] = useState([]);

  useEffect(() => {
    const search = async () => {
      const { data } = await axios.get("https://en.wikipedia.org/w/api.php", {
        params: {
          action: "query",
          list: "search",
          origin: "*",
          format: "json",
          srsearch: term,
        },
      });
      setResults(data.query.search);
    };
    search();
  }, [term]);

  const searchResultsMapped = results.map((result) => {
    return (
      <div className="item" key={result.pageid}>
        <div className="content">
          <div className="header">{result.title}</div>
        </div>
        {result.snippet}      </div>
    );
  });

  return (
    <div>
      <div className="ui form">
        <div className="field">
          <label>Search Term</label>
          <input
            className="input"
            value={term}
            onChange={(e) => setTerm(e.target.value)}
          />
        </div>
      </div>
      <div className="ui celled list">{searchResultsMapped}</div>
    </div>
  );
};

export default Search;

And it’s a good thing that this is being converted to a string, because if was actually being rendered as HTML, that could have scripts embedded inside of it, which immediately makes us vulnerable to XSS attacks. We never want to inject code from an outside source unless it is extremely trusted.

But we also don’t want to display the HTML tags in our search results, we want them to look nice. What do we do?

One solution to this problem would be to create a simple find and replace script for all the spans. That would be a fine solution, and it would be safe. But what if we want to take this string, and turn it into proper JSX?

Danger Zone

dangerouslySetInnerHTML

Here is something you never do if the content isn’t coming from a trusted source.

There is a somewhat hidden feature of React. I actually used this method to make this websites Gatsby templates. dangerouslySetInnerHTML is a React DOM Element that converts any HTML elements in the contents into actual HTML instead of converting it to a string. This is acceptable for this website because I am the only person who is capable of adding content.

You can read more about this DOM Element in the React Docs React Docs: DOM Elements

Keep in mind however that you should only use this in situations where you are 100% certain where all the content is coming from. Like if it’s coming from yourself. Never use this for anything with user inputs.

I repeat, this is not a method for avoiding XSS attacks, it is one of the most common ways to become vulnerable to them.

components/Search.js
import React, { useState, useEffect } from "react";
import axios from "axios";

const Search = () => {
  const [term, setTerm] = useState("React");
  const [results, setResults] = useState([]);

  useEffect(() => {
    const search = async () => {
      const { data } = await axios.get("https://en.wikipedia.org/w/api.php", {
        params: {
          action: "query",
          list: "search",
          origin: "*",
          format: "json",
          srsearch: term,
        },
      });
      setResults(data.query.search);
    };
    search();
  }, [term]);

  const searchResultsMapped = results.map((result) => {
    return (
      <div className="item" key={result.pageid}>
        <div className="content">
          <div className="header">{result.title}</div>
          <span dangerouslySetInnerHTML={{__html:result.snippet}}></span>        </div>
      </div>
    );
  });

  return (
    <div>
      <div className="ui form">
        <div className="field">
          <label>Search Term</label>
          <input
            className="input"
            value={term}
            onChange={(e) => setTerm(e.target.value)}
          />
        </div>
      </div>
      <div className="ui celled list">{searchResultsMapped}</div>
    </div>
  );
};

export default Search;

And we can see here that it does what we expect.

cleaned results on screen

injected spans

There are now span elements on the page with class “searchmatch”. Wikipedia does this because it makes it convenient to apply a style to the matching word in all the result snippets. This website also does something similar to this. If you use the search bar you will see the matching portion of your search term highlighted in the results.

Sanitizing Inputs

If you are wondering about sanitizing inputs like we did in our Express application Ncoughlin: Frosty CMS: Sanitizing Inputs 🧼 that actually is also not necessary in React. This is one of the things where React has taken an opinionated approach and sanitizes all inputs by default. That is why the HTML tags in the above example did not render in the first place. You must purposely un-sanitize things with dangerouslySetInnerHTML if you want anything from an outside source to be anything other than a string.

Github Repo

Ncoughlin: React Widgets