One easy way to inject malicious code in any Node.js application

Malicious Code Injections in Node.js

tl;dr

This article describes a method of injecting arbitrary code in Node.js modules. It does not encourage unethical behavior. The chain used to include instances of modules can be tampered to allow modification of required dependencies. Knowing this fact, a malicious attacker would be able to craft a module that can modify legitimate code. This malicious code could then be shipped as part of a widely used module on the NPM registry and any `require` call made after the first instantiation of the malicious module would be vulnerable to a potential code injection.

However, the method described in this article can also be used legitimately in some situations such as code coverage calculation, mocking or instrumentation.
Programmers have to take the time to look at the sources of 3rd party libraries they are using in order to know what to expect.

Definitions

  • “require chain”: the code executed between a call to the `require` function and the return statement of this function.
  • “module”: a set of code that can be instantiated in a runtime using the `require` function
  • “core module”: a module available from the Node.js standard library.
  • “external module”: a module downloaded from a registry such as NPM.
  • “internal module”: a module existing only from within the current project and instantiated using `require` and relative or absolute path to the module’s location.
  • “main module”: the entry point of a Node.js application. It is the file passed as an argument to the `$ node` executable.

Introduction

I have recently been given the task of finding a way to instrument all functions declared within a Node.js application. I came up with the following approaches:

  • Create an addon to V8 to track all functions.
  • Add hooks to any method that triggers asynchronous operations, and update the code before its execution.
  • Change the behavior of the `require` function to patch code at instantiation time.

The first method would require a fair amount of low-level programming and would probably not be portable to other Node.js runtimes such as ChakraCore. Hooking all asynchronous core methods is definitely possible (it will be even easier in the future with the release of “async_wrap” by the tracing working group.) However, if the behavior of a code chunk is modified, the modification needs to be reapplied each time the chunk is seen in the event queue which could lead to severe performances loss. Hijacking the require chain is not common, but there are still quite a few modules that use this strategy. How it works is that all modules required through `require` are patched at instantiation time. The major drawback of this method is that the main module of the application can’t be instrumented.

I decided to go with hijacking the require chain, since it has the least disadvantages, and another solution would be attempted later for instrumenting of the main module.

The require chain

The Node.js require chain is based on the core module named “Module”.

require chain

A simplified view of the require chain

Modules name can represent one of three things:

  • core module name
  • the name of a module in the path (i.e. within the “node_modules” directory)
  • a relative path to a local file

There are also three outcomes to a successful call to `require`:

  • the module is already cached, so the cached version is returned. Because of this, calls to `require` are singletons.
  • the module is a core module, so a precompiled version is returned. (core modules have their own cache space)
  • the module is not in the core, so the “_compile” method will run the code from the required file using the core module named “vm”, cache the result, and return it.

Hijacking the require chain

The require chain can be hijacked in various locations. I decided to do it at the “_compile” level, and have released a module called “compile-hook”. Its source code is pretty simple and can be found here:

The “_compile’ method is monkeypatched to add a transformation step to the code before instantiation.

Injecting malicious code

The “jsonwebtoken” module is a pretty popular package for managing authentication with JWT, so we will use it as an example to demonstrate how we can to inject code into this module to steam the secret keys used to sign tokens.
Here is a simple malicious module I created earlier:

And an example of an unfortunate victim application:

The output of this app is as follows:

$ node index.js
secret shhhhh
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0NjQ4OTQ5Mjh9.LvIWJiFCamu8azvnNgh8VvleVZUETNvDZRon1tKBImU

As you can see, the secret key given to the `sign` function is intercepted by the malicious module and printed to the console.

secret shhhhh

We merely display the secret here, but a real malicious script could post the secret keys to a remote server such as Pastebin.

Conclusion

Regarding the Sqreen Node.js agent, I ended up using a slightly different strategy than the one described in this article: the exported methods of the instrumented modules are not modified but wrapped. The wrapper is given the responsibility of the instrumentation and the original code stays untouched.

This article is mainly aimed at raising awareness within the Node.js community of this potentially malicious technique. We demonstrate that hijacking the require chain is pretty easy, and can lead to severe security issues in Node.js applications. Even if countermeasures are created, programmers should always take the time to manually look at the source code of any external dependencies they download from public registries. Any time source code or executable binary data is downloaded and run on a machine, it can potentially lead to security issues.

Some modules hijack the require chain for legitimate reasons. For instance:

  • njsTrace” instruments function calls this way.
  • the “lab” test runner uses this method to compute code coverage.

There is no easy way to prevent this technique from being used, the only simple solution is to carefully review each dependency and wisely choose which third party modules you use in your projects.

Feel free to send me any comments and remarks regarding this article. Signup for our Node.js private beta to protect your app from attacks at runtime. 

Thanks for reading,

Vladimir

Vladimir is a software engineer at Sqreen.io with a background in cyber-security. He is involved in diverse open-source projects in JavaScript (mostly within the hapijs project) and has recently contributed to the Node.js core. He is currently working at Sqreen and is responsible for the Node.js instrumentation.

8
Leave a Reply

avatar
7 Comment threads
1 Thread replies
0 Followers
 
Most reacted comment
Hottest comment thread
8 Comment authors
SqreenStephen HandleyQuentinAnton VSDaniel Earwicker Recent comment authors
  Subscribe  
newest oldest most voted
Notify of
Mike
Guest
Mike

This is a good reminder to be very wary of your dependencies. However, the title is misleading. This is not a way to inject code into any Node app, only an app that includes your malicious package. You can try to get developers to unknowingly include malicious code through a package manager in any language, so lets not make it sound like Node has a unique vulnerability in this area.

Vance Feld
Guest
Vance Feld

One easy strategy would be to produce a useful module that gets consumed by many popular libraries, then start introducing your malicious code. Trojan Horse at it’s best.

Aaron King
Guest

Most of the possible modules I use like this are dev dependencies, mocks etc. So even though they could ruin my day they likely wouldn’t ruin a server. But I get your point.

Daniel Earwicker
Guest

Another way to get malicious code into any app is to politely ask the maintainer of the app if they’ll let you put some malicious code in their app.

Anton VS
Guest
Anton VS

Yes, I will look at the source code of all my 388 modules immediately.

Quentin
Guest
Quentin

I could be mistaken but I think you could just have done

const jwt = require(‘jsonwebtoken’);
var origSign = jwt.sign;
jwt.sign = (data, key) => {
console.log(‘secret’, key);
return origSign(data, key);
}

Am I missing something?

Stephen Handley
Guest
Stephen Handley

In terms of prevention, maybe detection would be a good first step. creating a module that itself patched require(‘module’).prototype._.compile and just recorded calls to _compile (and then proxied to the original) within the codebase it is used in. then generate a report of the modules making those calls

Sqreen
Guest
Sqreen

Thanks Ersel, It’s actually also a feature that https://www.sqreen.com/ provides! Would love to hear your thoughts on it

You May Also Like