A server reinfected four times: the Next.js CVE we took two months to see
July 10, 2026
For nearly two months, a server under our operation was reinfected with a cryptominer over and over. We cleaned it thoroughly, hardened it, and within hours it was compromised again. We changed passwords, enabled two-factor authentication, reinstalled the operating system from scratch. Nothing stopped it. The hypothesis anyone would have accepted —that the provider's infrastructure was compromised— was false, and chasing it would have cost us a pointless migration.
This is the technical account of how we diagnosed it, what actually caused it, and the lesson that remains. I write in the first person plural because I did the work alongside Claude Opus 4.8, Anthropic's assistant, in a forensic investigation flow worth documenting in its own right.
The symptom
The pattern was always identical. A binary with a random name appeared in /root, a process pinned the CPU at 100% mining Monero, a backdoor user with UID 0 was planted, along with a malicious cron job and a systemd service disguised as a legitimate system process. The load average spiked to 4 on a 4-core server.
What was baffling was not the malware —XMRig is well known and easy to eradicate— but its persistence. Every time we cleaned, it came back. And it did so without leaving a trace of access: the authentication logs showed no SSH login from the attacker. No successful brute force, no added keys, no console sessions. The malware simply reappeared.
The wrong diagnoses
The first hypotheses were reasonable, and all turned out to be false:
- The provider's panel. Since there was no SSH login, we assumed they were coming in through the virtualization panel's serial console. We changed the panel password, enabled 2FA, disabled the console auto-login. It happened again.
- The email account. If they could reset the panel, maybe they controlled the recovery email. We audited the account thoroughly: no hidden forwarding rules, no malicious app passwords, 2FA active for years. Clean.
- A dormant implant. After the third cleanup we thought something was surviving the surgical eradications. So we reinstalled the entire operating system, from a clean image, and hardened it from minute one: SSH key-only, root locked, strict firewall,
fail2ban.
Fifty minutes after that clean reinstall, the server was reinfected again.
That fact changed everything. A freshly installed system, with key-only access and no exposed credentials, compromised in under an hour without a single login. The conclusion seemed inevitable: the problem was beneath the operating system, in the provider's hypervisor. The logical recommendation was to migrate to another provider immediately.
The right question
Before accepting that conclusion —expensive and irreversible— we stopped at an uncomfortable question: what if it isn't the infrastructure? What if it's one of the sites the server hosts?
The "compromised infrastructure" conclusion was the most comfortable, but also the one that closed the investigation. We chose to measure it against the evidence instead of assuming it. The key was in a data point we had in front of us but hadn't cross-referenced: the exact second the first malware binary appeared.
Using find, we reconstructed the file-creation timeline at the moment of reinfection:
2026-07-10 21:36:15 /root/z9lp89ac
2026-07-10 21:36:16 /root/0tb9x4eo
2026-07-10 21:36:18 /root/.xqnhcclswcgl
2026-07-10 21:36:19 /root/.drnvmazftoal
2026-07-10 21:36:19 /root/g0w78t3d/.../bsfubea5bpji <- the miner
The first artifact was born at 21:36:15. The next question was obvious: what was happening on the server at that exact second?
The forensics: log correlation
We checked nginx's access.log in that time window. Amid the usual noise of bots scanning WordPress paths, this appeared:
85.x.x.x - - [10/Jul/2026:21:36:15] "POST /es HTTP/1.1" 303 14034
85.x.x.x - - [10/Jul/2026:21:36:16] "POST /es HTTP/1.1" 303 14034
An external IP made a POST to a route of the Next.js application at the exact second the binary was born. And not just once: the same IP repeated the pattern seventeen minutes later, coinciding with a second wave of binaries we had also recorded. Each POST from that address corresponded, to the second, with a download of the miner.
It was no coincidence. It was an HTTP exploit. The vector was not the panel, nor the email, nor the hypervisor. It was the web application itself, reachable on port 443, responding to a malicious request with code execution.
The root cause: CVE-2025-55182
The site was running Next.js 15.4.2. That version, with App Router and React Server Components, is vulnerable to CVE-2025-55182, dubbed React2Shell: a remote code execution flaw with CVSS 10.0 severity, exploitable through POST requests that abuse the Server Actions mechanism (the Next-Action header and server component deserialization).
In plain terms: an attacker sends a carefully crafted request to any route of the application, insecure deserialization executes it on the server, and from there they deploy whatever they want. In this case, a cryptominer.
This explained every anomaly that had confused us for two months:
- No SSH login: the attacker never touched SSH. They came in over HTTP, like any visitor.
- It survived the cleanups: as long as the application stayed online on the vulnerable version, the bots that scan the internet re-exploited it.
- It survived the reinstall: restoring the site brought back the same vulnerable version. Fifty minutes later, a bot found it and exploited it again.
- Changing passwords and 2FA did nothing: they used no credentials.
- The historical timeline fit: the site had launched two days before the first infection. The bot found it almost immediately.
The "provider infrastructure" hypothesis was not only false: migrating providers would have changed nothing. The bot would have found the same vulnerable application at any IP.
The fix
The root correction was a single line in the dependencies:
- "next": "15.4.2"
+ "next": "^15.4.10"
Version 15.4.10 ships the patched React Server Components runtime. We updated, rebuilt, and verified the application still compiled with no code changes. Then we rebuilt the server one last time —this time knowing the vector was closed— and restored every site on top of the safe version.
The proof that it worked was empirical. The same request that previously returned 303 with a 14 KB exploitable payload now responds:
POST /es -> 404
The insecure deserialization no longer happens. And where the server used to reinfect within fifty minutes, more than an hour passed without a single indicator of compromise, with a watchdog checking every five minutes. The cycle was broken.
As prevention, we enabled Dependabot on the repository: vulnerability alerts and automatic pull requests for any future CVE. The next time a critical dependency is exposed, we will know before a bot does.
The role of AI in the diagnosis
It is worth being explicit about how we worked, because it is part of this firm's thesis: applied AI does not replace judgment, it amplifies it.
Claude Opus 4.8 ran the field investigation —forensic collection, log correlation, timeline reconstruction, surgical eradication, server rebuild, and restoration of eleven sites— holding the full context of the incident over hours. But the decisive turn was not technical: it was refusing to accept the first plausible conclusion. When the comfortable hypothesis was "it's the provider, we should migrate," the decision to measure that hypothesis against the evidence —the exact second in the log— came from questioning the diagnosis, not accepting it.
That is the collaboration that works: the machine holds the detail and executes without fatigue; the person insists on the right question. Neither alone would have closed the case in a day.
The lesson
If this incident leaves a single idea, it is this: do not accept the first plausible cause without measuring it against the evidence.
The "compromised infrastructure" explanation was consistent with every symptom. It fit. And it was false. What dismantled it was not a more sophisticated tool, but cross-referencing two data points we already had —a file's timestamp and a line from an access log— with the discipline of asking what else could it be? before making an irreversible decision.
In security, as in data architecture, the comfortable hypothesis and the correct hypothesis are rarely the same. The difference between them usually lies in a detail that is already in front of you, waiting for you to cross it with another.
And, concretely: if you operate a Next.js 15.x application with App Router, check your version today. CVE-2025-55182 is real, working exploits are circulating, and bots are already looking for it.
Rubén Bolívar is founder and principal architect of TheTreeWay. This investigation was carried out in collaboration with Anthropic's Claude Opus 4.8.