<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Muhammad Nauman Hafeez</title>
    <description>The latest articles on DEV Community by Muhammad Nauman Hafeez (@naum4n).</description>
    <link>https://dev.to/naum4n</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4126340%2Fb9016376-9587-4c7b-8314-761aa019b2d2.jpg</url>
      <title>DEV Community: Muhammad Nauman Hafeez</title>
      <link>https://dev.to/naum4n</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/naum4n"/>
    <language>en</language>
    <item>
      <title>A Poisoned npm Package Infected Our Production Server, Here's How We Found and Removed It</title>
      <dc:creator>Muhammad Nauman Hafeez</dc:creator>
      <pubDate>Wed, 16 Sep 2026 07:34:56 +0000</pubDate>
      <link>https://dev.to/naum4n/a-poisoned-npm-package-infected-our-production-server-heres-how-we-found-and-removed-it-2peo</link>
      <guid>https://dev.to/naum4n/a-poisoned-npm-package-infected-our-production-server-heres-how-we-found-and-removed-it-2peo</guid>
      <description>&lt;p&gt;How a routine deploy infected our CI server, how we hunted it down, and what actually fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup&lt;/strong&gt;&lt;br&gt;
Our on-prem Ubuntu server runs ~20 GitHub Actions self-hosted runners and a dozen Next.js apps under pm2. One shared box, many repos — remember that detail, it matters later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 0: "Is this suspicious?"&lt;/strong&gt;&lt;br&gt;
While running a routine security audit, one check jumped out — two strange processes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;node -e global.i='7-v1729';global.e='NPM';global.r=require;...&lt;/code&gt;&lt;br&gt;
Heavily obfuscated JavaScript, running as our deploy user, with established HTTPS connections to an unknown IP: 181.214.149.148:443.&lt;/p&gt;

&lt;p&gt;Deobfuscated, the loader did one thing: fetch JSON from a remote server and eval() whatever came back. A classic remote-code stager — the attacker could execute anything, anytime, on our production box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The forensics trail&lt;/strong&gt;&lt;br&gt;
What we ruled out, step by step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ SSH brute-force — auth logs clean, all logins from our LAN&lt;/li&gt;
&lt;li&gt;❌ Rogue SSH keys, extra root accounts, cron/systemd persistence — clean&lt;/li&gt;
&lt;li&gt;❌ Malicious install scripts — we reinstalled with ignore-scripts=true… and it came back anyway&lt;/li&gt;
&lt;li&gt;❌ App source code &amp;amp; workflow file — clean&lt;/li&gt;
&lt;li&gt;❌ NODE_OPTIONS/env injection, npmrc tampering, pm2 modules — clean&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What we confirmed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ The loaders spawned during npm install + build of one specific app, delivered through a CI deploy&lt;/li&gt;
&lt;li&gt;✅ The parent chain: pm2 → npm start → next-server → malware. Every app boot respawned it&lt;/li&gt;
&lt;li&gt;✅ global.e='NPM' — the malware itself tags its delivery vector: a poisoned npm package&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scariest moment: after a full wipe (rm -rf node_modules .next + fresh install), the malware respawned within a minute — spawned mid-install, then orphaned itself to PPID 1 so it survived pm2 stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The resolution&lt;/strong&gt;&lt;br&gt;
We never got the exact package name — and that's a realistic detail worth sharing. Here's what the evidence showed: a dependency version that was live-poisoned on the npm registry on deploy day, cached locally, and pulled/patched upstream within 48 hours (this is how real supply-chain waves work — npm yanks compromised versions fast). Once we cleaned the npm cache and reinstalled fresh, the exact same sequence came back clean — verified end-to-end with kernel-level auditd logging armed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually fixed it&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Contain: iptables DROP on the C2 IP (made persistent!), stop the infected app, kill the orphaned loaders&lt;/li&gt;
&lt;li&gt;Freeze the vector: stop ALL runners — deploy freeze until clean&lt;/li&gt;
&lt;li&gt;Wipe properly: rm -rf node_modules .next plus npm cache clean --force — the poisoned tarballs live in ~/.npm/_cacache and reinfect you from cache&lt;/li&gt;
&lt;li&gt;Rotate everything: GitHub PATs, runner tokens, org secrets, every .env, SSH passwords. The malware had 24h of access — cleanup does not un-steal secrets&lt;/li&gt;
&lt;li&gt;Verify with a real deploy, then bring runners back one at a time&lt;/li&gt;
&lt;li&gt;Schedule a full host rebuild — after a remote-code stager, "no evidence of a second stage" ≠ "no second stage"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Lessons learned&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ignore-scripts=true is not enough. It blocks install hooks but NOT require-time payloads that run when your app boots.&lt;/li&gt;
&lt;li&gt;Self-hosted runners are a blast radius multiplier. One poisoned dependency in ONE repo = full compromise of every project on the box. Isolate runners.&lt;/li&gt;
&lt;li&gt;npm's cache can reinfect you. Wiping node_modules without npm cache clean is not a clean install.&lt;/li&gt;
&lt;li&gt;Orphaned processes survive pm2 restarts. Check for PPID 1 processes you don't recognize.&lt;/li&gt;
&lt;li&gt;Block C2 IPs at the firewall AND persist the rule — an in-memory iptables rule dies on reboot.&lt;/li&gt;
&lt;li&gt;Rotate credentials immediately. This is the step everyone delays and the one that matters most.&lt;/li&gt;
&lt;li&gt;A ufw status that says inactive on a production server is a finding in itself. 🙃&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was my first incident of this kind — if you run self-hosted runners on a shared box, I hope you never need this post. But if you do: contain, freeze, wipe (cache too!), rotate, verify, rebuild.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>node</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
