I Spent 15 Days Chasing Malware Around My Dev Environment
A payload in a Tailwind config turned into a patched VS Code, a patched npm, and a persistence mechanism that was really just my own development workflow.
- Security
- Supply Chain
- Tooling
I did not expect to spend most of September investigating malware on my own laptop, especially malware I had already removed twice.
It started with a suspicious line in a Tailwind config file. From there it turned into a modified VS Code, patched npm, and several more infected repositories. For about two weeks I was doing detective work against my own development environment.
Here's how it went.
It started with a config file
I started getting notifications about failed Vercel deployments on repos I hadn't touched, on days I wasn't even using my laptop. A colleague had mentioned the PolinRider campaign around the same time and the indicators were easy enough to grep for, so I searched my repositories.
The first config file I opened looked completely normal, until I noticed I could scroll horizontally. It was a tiny file. There was no reason for it to be that wide. I dragged the cursor all the way right and found this sitting past the end of the visible code:
};
global['!']='9-0586-2';
var _$_1e42=(function(l,e){ ... });
I'd removed the same kind of payload from another project months earlier, which made the interesting question not "what is this" but "how did it come back?"
Deleting it wasn't enough
Git history had part of the answer. I'd removed the payload in a security commit, and eight days later it reappeared in a completely unrelated commit: 171 changed lines in an 88-line file. Something had rewritten it, and I had no idea what.
My first search had also been incomplete. git log -S never showed the original injection, because it happened inside a merge commit. Once I searched merge diffs as well, the missing commit turned up:
git log --all --diff-merges=on -S'<marker>'
So the malware had definitely been committed. I still didn't know what was executing it.
Then I found the processes
Three suspicious node -e processes were running, one of them parented by VS Code. The loader identified itself clearly enough:
global['_V']='A9-8166';
global['e']="app-vscode-eval";
It was fetching more code, decrypting it and executing it. That pointed me at the editor, where I found:
VS Code\resources\app\out\main.js
VS Code\resources\app\out\main.inz.cjs
VS Code itself had been patched, rather than an extension or anything inside one of my projects. There was also a main.js.inz.orig file holding a clean copy of the original entry point, which explained how the infection survived updates: the malware kept its own backup so it could re-patch the application afterwards.
"But my VS Code is signed"
Mine was too. The executable was signed and intact, but the malicious JavaScript lived under resources\app\, which isn't the executable, so the signature told me nothing useful. That was a fun distinction to learn while already dealing with malware.
I also found that the C2 infrastructure could be updated through Ethereum transactions, so I spent an afternoon inspecting blockchain transactions because of a Tailwind config file. I blocked the IPs and they changed.
Reinstalling didn't fix it
I removed VS Code (๐) along with its configuration and extensions, rebooted, reinstalled everything and checked again. Clean. Then it came back.
That's when I realised I'd been asking the wrong question the whole time. I kept asking how to remove it, when what I needed to ask was what kept putting it back. So I stopped reinstalling things and started following timestamps.
The actual loop
The chain turned out to be circular. An infected repository contained a malicious config file, I opened the project, development tooling evaluated the config, the payload executed and patched VS Code, and the patched VS Code could then run the loader again. The infected project and the infected editor were feeding each other.
There was no scheduled task and no startup entry. The persistence mechanism was my own development workflow, which is exactly why every standard persistence check came back clean.
Then npm joined the party
I put write protection around the VS Code files, and the next time a loader ran, VS Code stayed clean. The lock worked, but something else was obviously still infected:
global['e']='NPM';
global.i='A9-8166';
npm\lib\cli.js had been patched across multiple Node installations. One infected copy was over 1.4 MB, where a clean npm CLI is a few hundred bytes. Same victim ID, same loader.
That also explained why reinstalling Node hadn't helped. A malicious process that's already running can simply patch the fresh installation, so what I'd actually been doing was:
reinstall tool
โ
clean tool
โ
infected process still running
โ
tool gets infected again
Which was not particularly productive.
The second cleanup was different
This time I started with the repositories instead of the tools. I checked every project for the payload, went through Git history, and removed the infected configs. Only then did I clean the development tools: VS Code restored from the clean backup the malware had helpfully left behind, infected Node installations replaced, npm verified.
Then I added write protection to the application files. The first Windows permission rule I wrote broke VS Code completely, because "deny write" turns out to be less simple than it sounds. Once I got the permissions right, VS Code could read its own files and the malware couldn't modify them. It stayed clean after that.
I also found more infected repositories
Searching Git history across the repositories I had access to turned up several different victim IDs:
A9-8166
A9-0542-1
A9-0586-3
A9-9317-1
A10-*20930
Mine was A9-8166. Some repositories carried different IDs and slightly different generations of the payload, which let me separate the infections instead of assuming they all came from one source.
It also made the point I'd been missing: the repository is part of the attack surface. A malicious change can get into a project, execute through normal development tooling, and compromise the developer's environment from there. Cleaning my laptop was never going to be enough on its own.
What changed afterwards
I added a few layers around my environment. There's an integrity check that looks for suspicious processes, modified VS Code and npm files, known payload markers, odd whitespace in config files and the usual persistence mechanisms. Git hooks scan configuration files after merges and checkouts and before commits. The development tools are write protected. Branch protection means changes have to go through pull requests and security checks.
None of this is a magical malware prevention system. It just makes the same chain harder to repeat.
What I actually took away
I went into this thinking in terms of files: find the malicious file, delete the malicious file, done. That was never the problem. The problem was the chain:
repository
โ
configuration
โ
tooling
โ
Node
โ
editor
โ
repository
Once I started looking at that instead of at individual infected files, the behaviour finally made sense. Now when something suspicious shows up, I'm much less interested in where the malware is, and much more interested in what's capable of putting it back.
I'm currently open to engineering roles and select consulting work. If your team needs someone who can ship across the stack, let's talk.