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.
- “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.
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”.
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.
We merely display the secret here, but a real malicious script could post the secret keys to a remote server such as Pastebin.
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,