From Component to Compromised: XSS via React createElement
Sat Oct 11 14:49:09 UTC 2025
XSS in modern React apps isn't gone, it's just hiding in new places. In this blog, we'll expose how React createElement can be your way in.
We'll introduce several React DOM XSS lab scenarios based on real bug bounty findings from vulnerable applications in the wild. You'll see how untrusted input can make its way from a variety of realistic sources to a React createElement sink, leading to exploitable XSS, even in apps built with frameworks like Next.js.
These labs are realistic, grounded in actual bugs, and designed to sharpen your ability to spot and exploit DOM XSS in the kinds of apps bounty hunters hit every day.
Background
Back in April I put together a workshop for the Defcon Bug Bounty Village focused on scenarios where a user accessible source reaches the React createElement sink in some way, and how these scenarios can lead to cross site scripting or similar impact. After presenting it a few times since then and getting feedback on it, I've decided to put together a limited blog post about the content. I will not be covering full lab challenge walkthroughs here, or going through the full introduction to the topic available in the slides, but instead hitting some valuable highlights, and leaving the exercise of solving the challenges to the reader.
What is createElement in React?

React implements a createElement function different than document.createElement that is used internally and even offered externally for the generation of DOM elements.
How does JSX get compiled into React createElement calls?

Implementations vary massively between the latest versions of React and older versions that are still largely in use in the wild, but the usage of the React createElement function as a powerful sink still holds true.

In this example, we can clearly see how the JSX gets translated to createElement calls in the minified bundle that gets built.
Breaking down createElement's function signature

Type
- The first argument passed to createElement is the type to be created, which interestingly can be a number of values with different behaviors depending on the type of the type value
- Strings - creates an HTML element of that literal string type (i.e. "div" -> <div></div>)
- Functions/Classes - treats these as a React component definition and calls the appropriate code to construct and render an instance of these
- In scenarios where elements are dynamically created and type can be influenced by an attacker provided value, passing a string here instead of an expected React component can lead to unintended consequences with potential impact if more createElement arguments have some level of attacker control.
Props
- The second argument, props, is one of the better known injection points for attackers.
- An object or null is expected, and key/value pairs on this object will be assigned to the created element as props if the type is a React component, or HTML element attributes if the type is a string, with some restrictions.
- Certain special values exist, like the well known dangerouslySetInnerHTML field
- Control over certain fields of the props argument, the entire props argument, or an object spread to the props argument can be a very powerful tool for achieving XSS
Children
- The children argument(s) of createElement takes "React nodes"
- This can be a string literal that will be rendered as a text node
- This can be a React element object
- In modern React, this requires certain fields be set to certain Symbol values, preventing the ability to inject valid arbitrary React elements from deserialized JSON
- In much older React (Changed in 2015) validating these instead checks the _isReactElement: true field, allowing for arbitrary JSON to be deserialized into a valid React element, making this a much more powerful sink in ancient React versions.
Exploitation Cheat Sheet
Assuming attacker controlled deserialized JSON being passed into this function:

I'm not thrilled with this cheat sheet. It serves a great utility for this lab, but I think it is still lacking some nuances.
Lab challenges
The lab challenges are accessible at https://defcon.turb0.one. The goal of each one of these is to achieve JavaScript execution. Some of the challenges include source maps, some deliberately don't. These challenges will remain up until the end of October. After that, I intend to take the box down and leave a static web page up instead that offers the tarball download to still allow people to run these locally.
Most interesting lab challenge
Based on feedback I've received, one of the most eye opening and interesting pieces of these labs is that the following webpacked and transpiled React component can lead to XSS.

If this seems impossible, I recommend you go play around with the labs.
Further research ideas
There's a lot more to explore here. Here are some cool directions this could be taken in:
- CodeQL rule to detect tainted arbitrary prop object keys on childless React createElement calls
- How does this interact with server side rendering?
- Newer versions of React actually call a JSX fragment intermediary in places where createElement used to be called directly. It appears to ultimately have the same effect as the older versions, but are there interesting nuances here that may have been overlooked?
- What happens after createElement gets called? What underlying DOM API calls are used to create the actual elements, and what nuances exist here?