Ecosystem engineer at @github. Sometimes I write about JavaScript, Node.js, and frontend development.





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.

Prevention

There are some preventative actions you can take right now:

Lock your dependencies . Use 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.

. Use or to prevent getting automatic updates when deploying (when doing 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. Use npm audit, Snyk and/or GitHub security alerts to be notified when any of your dependencies could contain security vulnerabilities.

Mitigation

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:

{ "permissions": { "fs": { "directories": { "$TEMP": "rw", "$SRC_ROOT": "r" } }, "network": { "tcp": { "v4:*:$PORT": "LISTEN" } } } }

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!

Sandboxing your app — the hardcore way

Thanks to the dynamic nature of JavaScript that Node.js also follows, we are able to hack the runtime. We can:

Hijack the require() calls and manipulate the code that’s inside. That’s how ts-node/register and @babel/register work.

the calls and manipulate the code that’s inside. That’s how and work. Run code in a sandboxed environment with the vm module and pass a custom require function that prevents accessing certain modules, or wraps core modules to prevent accessing certain things.

OR

Just override the core modules, directly. Let’s look at how we can do this:

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:

// malicious.js const fs = require('fs') const secrets = fs.readFileSync('/system/secrets.txt', 'utf8') console.log(secrets);

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/ :

// cage.js const fs = require('fs') const path = require('path') const wrap = (module, name, wrapper) => { const original = module[name] module[name] = wrapper(original) } wrap(fs, 'readFileSync', (readFileSync) => (...args) => { const [filepath] = args const fullpath = path.resolve(filepath) if (fullpath.startsWith('/system/')) { throw new Error('You do not have permissions to access this file') } return readFileSync(...args) }) // Prevent further changes Object.freeze(fs)

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 .

Conclusions

Malicious code (or just vulnerable code) in our apps is a growing problem because our apps become more complex and rely on more dependencies, making the attack surface bigger and bigger

Services and tools such as npm audit, Snyk and/or GitHub security alerts are helpful and you can start using them right now

We need to mitigate the effects of an attack and Node.js needs to do something regarding that. However, the solution is not in the near future

If you want to go “the hardcore way”, you can! Node.js is flexible enough to allow you to do crazy stuff to protect yourself. We just demonstrated it 🙂

200’s only Monitor failed and slow network requests in production 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, https://logrocket.com/signup/ 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 apps, recording literally everything that happens on your site. 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. 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