Anatomy of a Breach: How a Next.js Vulnerability Turned Our Servers into Crypto Miners
A real-world account of discovering and fixing a critical Next.js RCE vulnerability that allowed attackers to install cryptocurrency miners on our Docker containers. Learn how npm audit and runtime security tools could have prevented this.
It's a humbling experience. As a senior engineer, you build redundant systems, write clean code, and mentor your team on best practices. You believe you have a robust, secure setup. Then, one Tuesday morning, the alerts start firing. CPU usage is pegged at 100% across several application containers. Response times are through the roof. Your gut tightens. This isn't a simple scaling issue—something is fundamentally wrong.
This is the story of how our team spent a frantic day hunting a ghost in our machine, only to find it was a vulnerability in one of the most popular and trusted tools in our stack: Next.js.
The Hunt: Chasing a Phantom Process
The initial symptoms were classic signs of a resource-hungry process. Our first suspect was a memory leak or an infinite loop in our latest deployment. We dove into the logs, profiled the application, but found nothing. The process list (ps aux) was our next stop. We scanned for anything out of the ordinary, but the process names were all familiar. The miner was hiding in plain sight—or, as we'd soon discover, wasn't visible from the host at all.
The breakthrough came when we noticed the resource spike was isolated to specific Docker containers running our Next.js frontend application. This allowed us to narrow our focus. We decided to inspect the running containers directly.
Our first command inside a suspect container was a simple ls -la /tmp. And there it was.
-rwxr-xr-x 1 nextjs nogroup 8297712 Nov 3 2024 app_worker
-rw-r--r-- 1 nextjs nogroup 3402020 Jan 15 11:52 b.tar.gz
lrwxrwxrwx 1 nextjs nogroup 47 Jan 14 15:40 xmrig.log -> ...app_worker, b.tar.gz, xmrig.log. The signature of a cryptocurrency miner. XMRig, to be precise. It was hijacking our server's CPU cycles to mine for Dogecoin.
The frustration was immense. We had found the "what," but not the "how." We stopped the container, and the /tmp directory was wiped clean, as expected. But upon restarting it, the files reappeared within seconds. The malware was being downloaded and executed at runtime, every single time the container started.
The Culprit: A Betrayal by a Trusted Friend
Our investigation turned to the application code itself. We had copied the entire /app directory from the container and started a forensic analysis.
- We checked our
docker-entrypoint.sh. It was clean. - We scoured our custom
server.js. Nothing. - We searched the entire codebase, including all of
nodemodules, for suspicious strings likeunmineable.comorapp_worker. Nothing.
This is the point where a junior engineer might give up. But when you've been around the block, you know the most insidious bugs often hide in the layers you take for granted. We turned our attention to our dependencies.
The package.json listed nothing out of the ordinary. But a dependency is a tree, not a list. The real test was a security audit. From within the application's code directory, we ran:
npm auditAnd there it was. A gut punch.
A critical Remote Code Execution (RCE) vulnerability in Next.js itself. Our application, running version 15.2.3, was in the affected range. An attacker could exploit this flaw to run arbitrary commands on our server. And they did, with a simple script that downloaded and launched their miner.
The disappointment is hard to overstate. This wasn't some obscure, unmaintained package from a random GitHub profile. This was Next.js, one of the most popular and well-regarded frameworks in the JavaScript ecosystem. It's a stark reminder: in modern software development, you are only as secure as the weakest link in your dependency tree.
The Lesson: Auditing is Not Optional
This incident drilled home a lesson we thought we already knew: automated security auditing is non-negotiable.
A simple npm audit or yarn audit command, integrated into our CI/CD pipeline, would have flagged this vulnerability before it ever reached production. It would have failed the build and forced us to update the dependency.
Moving forward, our pipeline will have a mandatory security audit step that blocks any deployment with critical vulnerabilities. There are no exceptions.
Proactive Defense: The Tools That Would Have Saved Us
Even with a vulnerability, the breach wasn't inevitable. A robust defense-in-depth strategy includes runtime security—tools that monitor your systems for suspicious behavior. Here are two open-source tools that would have caught this miner instantly, even with the vulnerability present:
1. Wazuh
A powerful, open-source security platform (a HIDS, or Host-based Intrusion Detection System). We could have configured a Wazuh agent on our server to monitor file integrity—it would have immediately alerted us when new files (appworker, b.tar.gz) were created in the /tmp directory. It also detects suspicious processes, flagging the execution of unknown processes like appworker.
2. Falco
A cloud-native runtime security tool, and a CNCF project. Falco is designed specifically for container security. It works by monitoring kernel-level system calls. It would have alerted us the moment the container tried to do something out of the ordinary—like a node process spawning an unexpected shell (sh), or an unexpected process (app_worker) opening a network connection to the mining pool.
Implementing one of these tools would have turned our multi-hour, high-stress investigation into a single, immediate, and actionable alert.
Conclusion: Stay Paranoid, Stay Safe
This incident was a painful but valuable lesson. We have since patched the Next.js vulnerability, rotated all our credentials, and are in the process of deploying Falco across our containerized environments.
Let this be a cautionary tale. Don't let your trust in a framework, no matter how popular, lull you into a false sense of security. Audit your dependencies religiously. Monitor your runtime environment suspiciously. Don't wait for your server's fans to start spinning at full blast to realize that the canary in your coal mine has stopped singing.