What is the XSS?
Cross-site scripting (XSS) accounts for the majority of web applications security vulnerabilities. It lets attackers inject client-side scripts into web pages, bypass access controls, steal sessions, cookies, connect to ports or computers camera.
Why we need to prevent XSS in React Applications?
React is a popular framework for building a modern JS frontend application. By default, data binding in React happens in a safe way, helping developers to avoid Cross-Site Scripting (XSS) issues. However, data used outside of simple data bindings often results in dangerous XSS vulnerabilities. This blog gives an overview of secure coding guidelines for React.
JSX Data Binding
To fight against XSS, React prevents render of any embedded value in JSX by escaping anything that is not explicitly written in the application. And before rendering it converts everything to a string.
A good example of how React escapes embedded data if you try to render the following content:
function App() {
const userInput = "Hi, <img src='' onerror='alert(0)' />";
return (
<div>
{userInput}
</div>
);
}
The output in the browser will be: Hi,,
rendered as a string with an image tag being escaped. That is very handy and covers simple cases where an attacker could inject the script. If you would try to load the same content directly in the DOM, you would see an alert message popped out.
Be careful when using dangerouslySetInnerHTML
Simple data binding does not work when the data needs to be rendered as HTML. This requires a direct injection into the DOM for data to be parsed as HTML, and it can be done by setting dangerouslySetInnerHTML:
const userInput = <b>Hi React</b>;
return <div dangerouslySetInnerHTML={{ __html: userInput }} />;
Without adequate security, rendering HTML causes XSS vulnerabilities. Always ensure the output is properly sanitized.
This will solve business requirements that let user directly style the text, but at the same time it opens a huge risk and possibility for XSS atacks. Now if the evil user enters <b>"Hi, <img src='' onerror='alert(0)' />"</b>
the browser will render this:
To avoid running dangerous scripts they should be sanitized before rendering. The best option is to use a 3rd party library, for example, popular and maintained library dompurify with zero dependencies sanitizes HTML. Improved code would now:
import createDOMPurify from "dompurify";
const DOMPurify = createDOMPurify(window);
function App() {
const userInput = "<b>Hi, <img src='' onerror='alert(0)' /></b>";
return (
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
);
}
When the developer forgets this, we can improve by ESLint rule:
$ npm i eslint eslint-plugin-jam3 -save-dev
Extend plugins the .eslintrc
config file by adding jam3
The plugin will check that the content passed to dangerouslySetInnerHTML is wrapped in this sanitizer function
{
"plugins": [
"jam3"
],
"rules": {
"jam3/no-sanitizer-with-danger": [
2,
{
"wrapperName": ["your-own-sanitizer-function-name"]
}
]
}
}
XSS when using html-react-parser
Libraries such as html-react-parser enable the parsing of HTML into React elements. These libraries avoid certain XSS attack vectors, but do not offer a reliable security mechanism. Always sanitize data with DOMPurify.
Avoid relying on HTML parsing libraries for security.
return (<div>{ ReactHtmlParser(data) }</div>);
Without proper sanitization, this pattern creates XSS issues.
Handling dynamic URLs
URLs derived from untrusted input often cause XSS through
obscure features, such as the javascript:
scheme or
data:text/html
scheme. Dynamic URLs need to be
vetted for security before they are used.
- Never allow unvetted data in an href or src attribute
return ( <a href={data}>Click me!</a> );
If possible, hardcode the scheme / host / path separator
var url = “https://example.com/” + data
This pattern guarantees a fixed destination for this URL
Use a URL sanitization library to sanitize untrusted URLs
Use DOMPurify to output HTML with dynamic URLs: DOMPurify removes HTML attributes that contain unsafe URLs
Server-side rendering attacker-controlled initial state
Sometimes when we render initial state, we dangerously generate a document variable from a JSON string. Vulnerable code looks like this:
<script>window.__STATE__ = ${JSON.stringify({ data })}</script>
This is risky because JSON.stringify() will blindly turn any data you give it into a string (so long as it is valid JSON) which will be rendered in the page. If { data } has fields that un-trusted users can edit like usernames or bios, they can inject something like this:
{
username: "pwned",
bio: "</script><script>alert('XSS Vulnerability!')</script>"
}
This pattern is common when server-side rendering React apps with Redux. It used to be in the official Redux documentation, so many tutorials and example boilerplate apps you find on GitHub still have it.
The Fix
One option is to use the serialize-javascript
NPM module to escape the rendered JSON. If you are server-side rendering with a non-Node backend you’ll have to find one in your language or write your own.
$ npm install --save serialize-javascript
Next, import the library at the top of your file and wrap the formerly vulnerably window variable like this:
<script>window.__STATE__ = ${ serialize( data, { isJSON: true } ) }</script>
Accessing native DOM elements
Traditional web applications suffer from DOM-based XSS when they insecurely insert data into the DOM. React applications can create similar vulnerabilities by insecurely accessing native DOM elements.
Avoid DOM manipulation through insecure APIs: innerHTML and outerHTML often cause DOM-based XSS
Scan your codebase for references to native DOM elements:
- React’s createRef function exposes DOM elements
- ReactDOM’s findDOMNode function exposes DOM elements
When DOM manipulation cannot be avoided, use safe APIs E.g., document.createElement instead of innerHTML
Reference: