We haven't sanitized the API data yet. This means we've left our users open to an XSS attack! Because I wrote the API, I know it's safe. But what if the API got hacked and the attacker added some malicious HTML strings to the data?
The attacker could insert JavaScript code that redirects our users to a malicious website. Or they could send all of our users' keystrokes to a remote server that the attacker controls. There's a whole host of bad things they could do!
To demonstrate this, I've created an endpoint that returns an unsafe HTML string. Don't worry, it won't do any real damage—although an attacker would say that! The value of the first object's .body
property contains a dodgy <img>
tag:
{
"userId": 1,
"id": 1,
"title": "<h2>sunt aut facere repellat provident occaecati excepturi optio reprehenderit</h2>",
"body": "<p>quia et suscipit</p><p>suscipit recusandae consequuntur expedita et cum</p><p>reprehenderit molestiae ut ut quas totam</p><p>nostrum rerum est autem sunt rem eveniet architecto</p><img hidden src='x' onerror='window.addEventListener(\"keydown\", (e) => void console.log(e.key))' />"
}
Let's examine it with HTML syntax highlighting:
<img
hidden
src="x"
onerror="window.addEventListener('keydown', (e) => void console.log(e.key))"
/>
hidden
attribute means the element will be invisible to our users.src
attribute means the element will fire an error
event.onerror
attribute adds an event listener that logs every keystroke to the console.Our users would be none the wiser if we accidentally inserted this element, but all their keystrokes would be logged to the console! Logging them to the console is harmless, but what if the attacker sent them to a remote server?
Before we render the third-party data, we must sanitize it! There is a native HTML Sanitizer API in the works, but it's still experimental at the time of writing. Until it becomes widely available, we can use DOMPurify. This is a library that's been thoroughly vetted by web security experts.
Here's what we need to do:
npm install dompurify
).DOMPurify.sanitize()
method in our util.js
file.dirty
parameter before setting the __html
property.DOMPurify will remove all dangerous attributes, including the onerror
attribute on the <img>
tag.
import { sanitize } from "dompurify";
function createMarkup(dirty) {
return { __html: sanitize(dirty) };
}
export { createMarkup };
Demo of safely rendering HTML in React.
Avoid setting HTML from code if possible. It's much safer to create the virtual DOM nodes yourself and insert only plain text. If you must insert an arbitrary HTML string, you can use the dangerouslySetInnerHTML
prop. You must sanitize the string before inserting it, otherwise you risk exposing your users to a cross-site scripting (XSS) attack. Until the HTML Sanitizer API becomes available, you can use DOMPurify.
For more information, see How to reduce your risk of cross-site scripting attacks with vanilla JavaScript.