You have probably heard about a recent incident where a popular npm package, event-stream, included malicious code that could have affected thousands of apps (or more!). Hopefully, the attack was tailored to affect only a specific project.
The original author of the library was the victim of a social engineering attack and a malicious hacker gained publishing permissions. Many people argue that the original author should have been more cautious.
But that’s not the real problem.
Why?
Because the original author of the library could have published the malicious code intentionally, anyone who owns a library could publish malicious code at any time. A lot of us are relying on the honor system, hoping that no one will publish malicious code.
How can we prevent that?
Well, there’s always going to be multiple ways of hacking the system and injecting malicious code into our apps. Not only through dependencies but also through unintentional vulnerabilities.
However, we can still think about how to prevent these things from happening but more importantly, we need to think about ways of mitigating their effects.
There are some preventative actions you can take right now:
package-lock.json
or yarn.lock
to prevent getting automatic updates when deploying (when doing npm/yarn install
in your server). At least this way you will get fewer chances of getting a malicious update that the npm team hasn’t cleaned up yet. However, this wouldn’t have prevented the event-stream from affecting you since the malicious code was available in the npm registry for weeks. But it probably would have prevented you from a separate incident back in July.Now, how can we mitigate the effects of an attack once it’s triggered?
Well, most attacks consist of stealing data, mining and sending back the results to a server, etc. So you could execute your Node.js with a user with very limited permissions: restrict filesystem access, configure iptables to restrict the application to only connect to certain domains, etc. The problem is that in the era of cloud services you probably can’t do that in your cloud provider.
Is there anything we can do inside Node.js?
The Node.js contributors have already started thinking about a Node.js Security Model. So, we can expect different levels of security to be implemented inside Node.js in the future.
I personally would love a permissions system where you could define what things you need to access in your package.json
. For example:
This would be something like the Content Security Policy we have in modern browsers.
But of course, this is just my suggestion and the Node.js Security Model idea is just starting to be evaluated. Don’t expect an implementation in the near future.
So, is there something we can do right now? And more specifically, is there anything we can do in Userland without changing the Node.js internals?
The answer is yes!
Thanks to the dynamic nature of JavaScript that Node.js also follows, we are able to hack the runtime. We can:
require()
calls and manipulate the code that’s inside. That’s how ts-node/register
and @babel/register
work.require
function that prevents accessing certain modules, or wraps core modules to prevent accessing certain things.OR
I’m going to show a proof of concept of overriding readFileSync
to prevent accessing files in a specific directory. In practice, we should override a few other functions and we also have the option of whitelisting instead of blacklisting certain directories.
But as an example, I just want to prevent malicious code:
I’m going to implement a cage.js
file that overrides the fs
core module and I’m going to intercept that function and prevent accessing files inside /system/
:
Voilá! There it is. Now if we run the malicious code directly:
node malicious.js
We will see the contents of that file printed to the stdout. But if we tell Node.js to first run cage.js
like this:
node -r cage.js malicious.js
We will see that the malicious code was not able to access the content of the file and an error was thrown.
Obviously, this is just a proof of concept. The next step would be to override more functions, make it configurable instead of hardcoding file paths, and, ideally, do the same with other core modules. For example overriding http(s).request .
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowMatcha, a famous green tea, is known for its stress-reducing benefits. I wouldn’t claim that this tea necessarily inspired the […]
Backdrop and background have similar meanings, as they both refer to the area behind something. The main difference is that […]
AI tools like IBM API Connect and Postbot can streamline writing and executing API tests and guard against AI hallucinations or other complications.
Explore DOM manipulation patterns in JavaScript, such as choosing the right querySelector, caching elements, improving event handling, and more.
2 Replies to "How to protect your Node.js applications from malicious dependencies"
This method is good for standard methods, but do you know what is a good way to block calls at the system level? When calls reach the v8 engine or uv, it should be able to implement a gating mechanism where the user can be asked consent.
This model is similar to android apps where we are told the permissions that the app requires in advance, and any additional access is denied till the user explicitly approves it.
I actually created a library that does something very similar to this, but uses a more sensible approach for permissions. It also differentiates between 1st/3rd party code so that your main application doesn’t have to jump through hoops https://github.com/yaakov123/hagana