XSS in modern React apps isn't gone, it's just hiding in new places. In this workshop, we'll expose how React createElement can be your way in.
We'll walk through 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.
The format of the session will be the following, with rough time breakdowns that are flexible based on how much time is allotted to this workshop:
- Intro
- What is React?
- What is createElement in React and how does JSX implicitly call it?
- What inputs does createElement in React take?
- How can each of these inputs lead to XSS?
Delve into four challenges ranging in complexity and difficulty based on real life findings in real applications that have resulted in real bounty payouts. I will be walking around and bothering people as they work on the challenges, prodding people in the right direction as needed, and sharing any hints/clarifications that come up to the group.
Walkthroughs of solutions of some of the challenges and explanations as to how they work and how they were meant to be solved.
Spoilers below! Don't read this if you're interesting in solving these challenges for yourself blind without hints!
Here are the lab challenges I've built for this workshop:
- Level 0
- Source Maps enabled
- Static React SPA
- Values from a query param are used to construct a string that reaches a dangerouslySetInnerHTML sink, most of them are sanitized by DOM Purify, but one value gets added after the sanitization, allowing for attacker controlled data entering this dangerous sink
- Sink into an existing dangerouslySetInnerHTML.__html string value.
- Level 1
- Source Maps enabled
- NextJS app
- Client side path traversal via path slug for a client side rendered page that passes data from the response in such a way that arbitrary user controlled data from an open redirect is merged with the props of a meter element allowing arbitrary property injection from an attacker resulting in XSS
- Sink into arbitrary key/value pairs spread on the props argument passed to React createElement
- Level 2
- No source maps
- React SPA
- Express JS backend API
- Rich JSON rendering of custom media component wrappers
- API call stores/retrieves arbitrary JSON that gets used to render a list of custom media components
- Sink into arbitrary component argument passed to React createElement, allowing arbitrary HTML element rendering, including frames/iframes with an arbitrary src property
- Level 3
- No source maps
- React SPA
- Express JS backend websocket API
- Minimalistic chat app
- Rich messages that include an animated reaction component that allows for attacker provided rich messages broadcasted to all members of a chat across the websocket to be passed as React createElement component and props arguments
- Optional CSP bypass "secure" mode:
- Content can be served with a CSP that limits scripts to self and a secure nonce
- Two injections can be performed by sending a victim two messages:
- The first, an inline style block that leaks a script nonce on the page via sequential import chaining
- The second, an iframe with a srcdoc that includes a script tag using the leaked nonce to get script execution