
I didn’t expect my weekend to be wrecked by a Monero miner. But here we are.
This is the post-mortem of how my Next.js-based side project, Kuray.dev, got nuked by a single, unauthenticated HTTP request. The root cause? A brutal new RCE vulnerability called React2Shell (CVE-2025-55182), rated a perfect 10.0 on the CVSS scale. But the real damage wasn’t just in the CVE itself — it was in how I built and deployed the system.
This isn’t a finger-pointing exercise. If anything, it’s me pointing fingers at myself. This happened because of decisions I made: trusting new tech too early, skipping some hard security best practices for short-term speed, and — let’s be honest — not fully understanding how deep the rabbit hole of React Server Components (RSC) actually goes.
Let’s walk through it.
Kuray.dev was built with what I thought was a smart, modern, lean stack:
Framework: Next.js 16.0.6 with the new App Router
Core Feature: React Server Components (RSC) enabled by default
Runtime: Node.js running on a basic Ubuntu VPS
Privilege Level: … root. Yeah, root. I know.
Everything was optimized for performance and DX. RSC felt like magic: stream partial UI from the server, no waterfall fetching, everything was snappy.
But magic comes at a price. And I didn’t read the fine print.
The CVE dropped in early December 2025, and bots started scanning for it within hours.
React2Shell is stupidly elegant in its attack vector. Here’s how it works:
A user hits an RSC endpoint (like /_next/flight or an RSC action).
The server starts to deserialize the Flight protocol payload — essentially converting streamed binary into JS structures.
But here’s the kicker: the deserialization logic is unsafe. It doesn’t properly sandbox the input.
The attacker sends a poisoned payload — looks like valid Flight data, but under the hood it’s building executable JavaScript objects.
Arbitrary code execution is triggered, server-side, unauthenticated.
And if your server is running as root — like mine was — that shell is the whole box.
Once the attacker had RCE, things moved fast. Forensics suggest a very typical miner deployment chain. Here’s what likely ran on my box:
The tarball dropped a set of files into /tmp:
.pwned — a marker file so bots don’t reinfect
xmrig-6.24.0/ — the actual Monero miner
sex.sh — the loader script
That script didn’t just run the miner. It installed persistence mechanisms, scheduled reboots, and renamed binaries to mimic legit system services.
Because the app was running as root, the attacker could do basically anything. And they did:
Cronjob Persistence
Fake Daemons
They created hidden-looking directories like /root/.systemd-utils/, then renamed the miner binary to ntpclient.
System Path Abuse
Dropped binaries into /etc/rondo, mimicking obscure system services. If I hadn’t known what to look for, I might’ve missed it.
The miner ran quietly in the background, eating CPU, blending into process listings, waiting for reboots, restarting itself like a zombie.
This part is on me.
Why was my Node.js process running as root? Because I didn’t want to deal with permissions. Because I wanted fast deploys. Because I was lazy.
Let’s be honest: it’s common in indie dev setups. You spin up a VPS, install Node, clone your repo, pm2 start, and call it a day.
I told myself I’d lock it down “later.” But later never came.
So when the RCE landed, it didn’t just get control of my app. It got the whole system. sudo wasn’t even needed — the attacker was already root.
Here’s what had to happen after the incident:
Kill the box. After a root-level compromise, the system is untrustworthy. OS wipe, clean reinstall.
Rebuild the environment to run under a dedicated, unprivileged nodeuser, with strict perms.
Patch everything. Upgraded Next.js and React to versions that fixed CVE-2025-55182.
Add firewall/WAF rules. Blocked public access to /_next/flight and other implicit RSC endpoints.
Segregate runtime and system. Moved the app into a container with no access to the base OS.
I also added system-level file integrity checks. Not because it’ll stop another RCE, but because next time I want to know when I’ve been owned.
This wasn’t just a bug. It was a collision of culture, tools, and shortcuts.
| Design Decision | Seemed Like... | Resulted In... |
|---|---|---|
| Adopting React Server Components | A modern performance win | Added attack surface via Flight Protocol deserialization |
| Running Node.js as root | Easy deployment | Turned RCE into full system takeover |
| No isolation between app and system | Simpler infrastructure | Allowed miner to persist and hide in system paths |
I wasn’t hacked because my code was bad. I was hacked because I assumed the ecosystem had my back — that RSCs were safe because they shipped in a major release, that pm2 would sandbox things, that nobody was scanning small projects.
All of that was wrong.
What happened to me is happening to a lot of people. React2Shell hit at a time when RSC adoption was growing fast, and when you give a global botnet a pre-auth RCE, they don’t care if you’re running a tiny blog or a national system.
What it exposed is something deeper:
The frontend world is moving faster than security can follow.
Default secure is still not a thing.
And everyone is one misconfigured pm2 away from disaster.
Kuray.dev is back online now — fresh OS, hardened pipeline, updated dependencies.
But I lost more than uptime. I lost a bit of trust in my own setup. That “it won’t happen to me” mindset is gone. Now every deploy feels like I’m inviting the world to test my assumptions.
If you’re running Next.js + RSC right now, go check:
Are you exposing /_next/flight or /api/rsc without auth?
Are your apps running under root?
Have you patched for CVE-2025-55182?
If the answer to any of those is no, assume you’ve already been compromised.
I should have.
Stay updated! Get all the latest and greatest posts delivered straight to your inbox