Securing Node.js Applications with Intrinsic

Thomas Hunter II
intrinsic
6 min readOct 1, 2018

--

In this post we’re going to secure a Node.js application using Intrinsic. In the process, we’ll introduce Intrinsic security policies and describe how Intrinsic enforces them.

Threat model: Intrinsic assumes that modules with malicious intent can find their way into Node.js applications. The typical Node.js app is about 95% third party modules. Scrutinizing every line of third party code simply isn’t a realistic way of protecting yourself from these threats, which is why Intrinsic is here to protect you.

Intrinsic protects your applications by applying the principle of least privilege. This means that by default your application isn’t allowed to perform potentially dangerous operations, such as talking to the filesystem or network. To enable such functionality in a safe way you can configure Intrinsic by defining policies. These policies offer a fine-grained interface for enabling exactly the I/O you expect your application to perform.

To start, let’s consider a very basic web application implemented in Express. This app has a GET route that makes an outbound request to a private, third-party service and responds to the user with the content it receives from this service. It also has a second route which simply responds with the content from a text file.

This application makes use of two Express middleware: the popular body-parser, for parsing incoming JSON requests, and express-faster-magic, the newest and coolest fictional middleware which allegedly makes requests faster.

Suppose the company we’re writing this app for uses a tool to test Blue/Green deployments. It does this with a header called X-BlueGreen-Host-Override. This header is removed from requests originating outside of the company by an intermediary web server, like nginx, for security purposes. The header might look like this:

The header is used for overriding known host names. The company website is example.org, and each of the services it communicates with are subdomains of that domain.

The sample application code looks as follows:

Note that the apps sends sensitive credentials, stored as secret, to the third-party service. Normally this is OK because we intend to only send this secret to internal third-party services.

But, suppose that one day, a new public-facing API is introduced. This new API isn’t aware that it needs to strip out the Blue Green header. Because of this, third parties are able to set the header and pass it to the Node.js application. An evil header might look as follows:

When the attacker sets the Blue Green header value they’re able to send the private credentials to an evil third party service.

Let’s now use Intrinsic to run this app with least privilege, i.e., let’s restrict the app to only have access to resources it needs. To this end, let’s first examine the sort of I/O we expect the application to perform.

Anticipating Application I/O

Intrinsic works by protecting your application at the layer where it interacts with the world around it. Intrinsic is able to make these protections on a per-route basis, assuming you’re building a traditional route-based application. (If you are building a serverless application then it’s even simpler to configure).

For most applications, figuring out what privileges a route needs (in essence what I/O must be allowed) is pretty straight forward. Our application, for example, needs to perform four kinds of I/O:

The first set of I/O operations are the HTTP requests entering the system. These are what trigger the callbacks. In this application we have two incoming routes: the more complex GET /retrieve-data and the simpler GET /read-file.

The second I/O operation happens every time the application runs. This I/O is a read operation on the ./credentials.json file.

The third I/O operation is an outbound HTTP request. Perhaps in the case of this service we will only make requests to a single server, user1.example.org. This means that the full request which we will communicate with will be POST http://user1.example.org/data-from-third-party.

The fourth I/O operation is in the second route, GET /read-file, which only needs to read the file my-file.txt.

Now that we know what I/O the application should perform, let’s look at how to configure the application to load, and be secured by, Intrinsic.

Configuring Intrinsic

To use Intrinsic you don’t need to modify your application code. All you have to do is add two new files: a wrapper file that loads Intrinsic before your app, and a policy file that defines what your application can and can’t do.

This first file, which we’re naming intrinsic.js, will serve as our new entry-point:

While normally you’d execute the previous server.js script to run your application, with Intrinsic you’re going to execute intrinsic.js.

This second file is named intrinsic-policy.js and will contain our policies:

There are three sections to note in this policy file. The first one is routes.allRoutes and the other two are routes.get() sections.

Earlier we determined that the credentials.json file is always being read when the application starts. This means if we add more routes in the future they will each be required to have read access to the same file. Because of this we put that policy in the allRoutes section.

Next we have the sections for the two inbound GET routes. These routes both have different behaviors; the first makes outbound HTTP requests while the second reads from the filesystem. Because they differ we have two route-specific policies to configure.

You’ll also need to modify your package.json file to point to the Intrinsic module. When you sign up for Intrinsic we’ll provide you with a tarball of the module. Here’s what the additional line in your package.json file might look like:

This tarball can be uploaded to a private corporate npm registry or simply included within your application in the filesystem. Once you have the tarball you can run your installation using the familiar npm install command.

Protecting against Developer Mistakes

The aforementioned error with the application, where the originating request gives us a hostname to communicate with, is what we’ll first be considering.

Ideally, a trusted consumer of our service will make a request akin to the following cURL sample:

However, perhaps one day an attacker generates the following request:

Without Intrinsic protecting our application the above request will succeed, transmitting our secret credentials to a malicious third-party service. This is because an attacker can set the X-BlueGreen-Host-Override header to a domain they control.

However, now that we have Intrinsic running, the above request will fail, and the following violation will be logged:

Protecting against Malicious Modules

Remember the express-faster-magic module we loaded in our application? Unfortunately, the module has just been compromised!

The module has been modified so that with every request entering our application, a new outbound request is being made to an evil third party server. That request will send our environment variables, as well as a copy of the incoming request body — which may contain sensitive data such as a users login data.

Here is what the module is now doing:

Though this attack may seem far-fetched, let me assure you that it is completely feasible. This example is modeled after a real attack from a module which was recently published to the npm repository. For more information, check out our post: The Dangers of Malicious Modules.

Without Intrinsic running to protect the application those malicious requests can be sent freely. With Intrinsic running the requests will not only fail but will also be logged.

What does Intrinsic Protect?

Intrinsic protects at a pretty deep layer in the application; it runs between application code and the Node.js APIs. Because of this we’re able to interpose on these APIs and either allow or deny I/O operations before they happen. We offer policies for the following types of operations:

  • Child Process Execution
  • Filesystem Operations
  • Outbound HTTP Requests
  • TCP/UDP Communication
  • Potentially dangerous process features, such as process.kill()
  • Databases, such as MongoDB or PostgreSQL
  • Tokenization, such as AWS Environment Variables

Request a Demo of Intrinsic

Visit our Get in Touch page if you’re interested in trying a demo of Intrinsic.

This article was written by me, Thomas Hunter II. I work at a company called Intrinsic (btw, we’re hiring!) where we specialize in writing software for securing Node.js applications. We currently have a product which follows the Least Privilege model for securing applications. Our product proactively protects Node.js applications from attackers, and is surprisingly easy to implement. If you are looking for a way to secure your Node.js applications, give us a shout at hello@intrinsic.com.

--

--