Our Background On Writing Clean Node.js Applications

Here at Twistlock, we use containers and Node.js applications in our own development. Similar to our customers, we face the same challenges in trying to make sure everything in our containers is clean and free of vulnerabilities. In this blog, I will detail a project that I went through recently to eradicate vulnerabilities from the Node modules that we use inside the Twistlock Console application.

The Twistlock Console application is one of the main applications in the Twistlock Container Security Suite. Among other functions, the Console communicates with Twistlock Defenders installed on each application host that we protect. The Console gathers runtime information from the Defenders and instructs the Defenders in terms of security policies and response actions. Note that both the Console and Defenders are containerized applications.

To provide context for the rest of the blog, here is the Dockerfile which shows how we built the node.js application.

FROM alpine:latest
RUN apk add –update-cache –repository http://dl-3.alpinelinux.org/alpine/v3.4/main/ –allow-untrusted && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY . /usr/src/app/
CMD [‘node’, ‘server.js’]

Our package json:

/myProject # cat package.json
{
“name”: “myproject”,
“version”: “1.0.0”,
“description”: “”,
“main”: “index.js”,
“scripts”: {
“test”: “echo \”Error: no test specified\” && exit 1″
},
“author”: “”,
“license”: “ISC”,
“dependencies”: {
“socket.io”: “1.4.8”
}
}
As shown, we use ‘socket.io’ v1.4.8 in our application. Socket.io is a websocket framework that we use to communicate with the Twistlock Defenders.

Detecting Vulnerabilities in Node

In order to make sure that the Console application is free of known vulnerabilities, I used Twistlock Scanner to scan the Console container, and found a list of vulnerabilities associated with ‘socket.io’ . The highlighted window in the Twistlock screen shot below shows the vulnerabilities found.

EranBlogimage01_highlighted

More specifically,

  • ws: Ws is a package versions 1.10 or earlier are vulnerable to potential DOS attacks. If someone sends an excessively long payload to the server, they can cause the server to crash, thereby causing denial of service. More information about this vulnerability can be found here
  • tough-cookie: versions between 0.9.7 and 2.2.2 (inclusive) are vulnerable. It contains a vulnerable regular expression that, under certain conditions with long strings of semicolons in the “Set-Cookie” header, can cause the event loop to block for excessive amounts of time. More information about this vulnerability can be found here.
  • negotiator: versions 0.6.0 and prior are vulnerable. The header for “Accept-Language”, when parsed by negotiator, is vulnerable to Regular Expression Denial of Service via a specially crafted string. More information can be found here
  • minimatch: versions 3.0.1 and prior are vulnerable to ReDoS in the pattern parameter. This is because of the regular expression on line 521 of minimatch.js: /((?:\\{2})*)(\\?)\|/g,. The problematic portion of the regex is ((?:\\{2})*) which matches against \\. More information can be found here

As an example, here is what Twistlock has to say about the ‘ws’ vulnerability.

image00

 

Analysis of The Vulnerabilities And Where They Came from

By invoking npm list, I can build my dependency tree as shown below. I listed the vulnerable packages in red.

/myProject # npm list
myproject@1.0.0
└─┬ socket.io@1.4.8
├─┬ debug@2.2.0
│ └── ms@0.7.1
├─┬ engine.io@1.6.11
│ ├─┬ accepts@1.1.4
│ │ ├─┬ mime-types@2.0.14
│ │ │ └── mime-db@1.12.0
│ │ └── negotiator@0.4.9
│ ├── base64id@0.1.0
│ ├─┬ engine.io-parser@1.2.4
│ │ ├── after@0.8.1
│ │ ├── arraybuffer.slice@0.0.6
│ │ ├── base64-arraybuffer@0.1.2
│ │ ├── blob@0.0.4
│ │ ├── has-binary@0.1.6
│ │ └── utf8@2.1.0
│ └─┬ ws@1.1.0
│ ├── options@0.0.6
│ └── ultron@1.0.2
├─┬ has-binary@0.1.7
│ └── isarray@0.0.1
├─┬ socket.io-adapter@0.4.0
│ └─┬ socket.io-parser@2.2.2
│ ├── debug@0.7.4
│ └── json3@3.2.6
├─┬ socket.io-client@1.4.8
│ ├── backo2@1.0.2
│ ├── component-bind@1.0.0
│ ├── component-emitter@1.2.0
│ ├─┬ engine.io-client@1.6.11
│ │ ├── component-inherit@0.0.3
│ │ ├── has-cors@1.1.0
│ │ ├── parsejson@0.0.1
│ │ ├── parseqs@0.0.2
│ │ ├── ws@1.0.1
│ │ ├── xmlhttprequest-ssl@1.5.1
│ │ └── yeast@0.1.2
│ ├── indexof@0.0.1
│ ├── object-component@0.0.3
│ ├─┬ parseuri@0.0.4
│ │ └─┬ better-assert@1.0.2
│ │ └── callsite@1.0.0
│ └── to-array@0.1.4
└─┬ socket.io-parser@2.2.6
├── benchmark@1.0.0
├── component-emitter@1.1.2
└── json3@3.3.2

You can see that ‘ws@1.0.1’ is part of the dependency of ‘engine.io-client@1.6.11’, which is a dependency of ‘socket.io-client@1.4.8’, which is a dependency of ‘socket.io@1.4.8’.

Similarly, ws@1.1.0 is a dependency of ‘engine.io@1.6.11’, which is a dependency of ‘socket.io@1.4.8’. And
‘negotiator@0.4.9’ is a dependency of ‘accepts@1.1.4’, which is a dependency of ‘engine.io@1.6.11’, and in turn a dependency of ‘socket.io@1.4.8’.

The reason for the minimatch and tough-cookie vulnerabilities is that installing the node.js package usually requires the installation of certain global modules to ‘/usr/lib/node_modules/npm’, and some of these global modules are vulnerable.

Indeed we can verify that by searching for such folders in our container:

/myProject$ docker run –rm -ti examples/myproject:1.0 /bin/sh
/usr/src/app # npm list -g | grep -e minimatch -e tough-cookie
| `– minimatch@3.0.0
| +– minimatch@3.0.0
| | +– minimatch@3.0.0
| | `– minimatch@2.0.10
| +– minimatch@1.0.0
| | +– minimatch@3.0.0
| +– tough-cookie@2.2.2

Fixing Vulnerabilities In Node

The first step of remediation is to remove all globally installed modules. We can do that using
`rm -rf /usr/lib/node_modules/npm/`,

Because default installed global modules may have vulnerabilities, we need to replace them individually with updated modules.

This is our revised Dockerfile:

FROM alpine:latest
RUN apk add –update-cache –repository http://dl-3.alpinelinux.org/alpine/v3.4/main/ –allow-untrusted && rm -rf /var/lib/apt/lists/* /usr/lib/node_modules/npm /usr/bin/npm
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY . /usr/src/app/
CMD [‘node’, ‘server.js’]

At this point, we’re left with three vulnerable packages: ws@1.0.1, ws@1.1.0 and negotiator@0.4.9
There are two potential options to fixing these vulnerabilities:

  • Option 1: Update ‘socket.io’ to the latest version, which hopefully no longer contains vulnerable sub-modules.
  • Option 2: Tweak sub-dependencies directly inside ‘socket.io’ to include the latest, patched modules.

Looking at npmjs for ‘socket.io’, we can see that we are already using the latest version of ‘socket.io’.

So that left us with option #2, which is to update sub-dependencies directly inside ‘socket.io’. Once the sub-dependency modules are updated, we can use ‘npm shrinkwrap’ to force our changes so that npm won’t revert the changes in the future. ‘Shrinkwrap’ is a feature in npm that allows us to save the modifications without committing the changes (although it is suggested to always do a pull-request to the module owner) or saving the modules in our git repository.

Using ‘shrinkwrap’ we can modify versioning of nested dependencies without having it being overwritten during npm install. Assuming the versioning update is minor, it may not break anything. But you must always test to verify backwards compatibility.

Updating Sub-modules

As an example, I will describe how I updated ws@1.0.1 to ws@1.1.1.

According to nodesecurity, ‘ws@1.1.1’ fixed the aforementioned ‘ws’ vulnerability. Recall that ws@1.0.1 is a dependency of engine.io-client@1.6.11, which is a dependency of socket.io-client@1.4.8, which is in turn a dependency of socket.io@1.4.8. To update ws@1.0.1, we can either look for an updated engine.io-client that is packed with a newer version of ws module, or replace the ‘ws’ module directly. At the time of writing, engine.io-client@1.6.11 is the latest, so I proceeded to update ws directly:

/myProject/node_modules/engine.io-client$ cat package.json | grep ws
“browser”: {
“ws”: “1.0.1”,
/myProject/node_modules/engine.io-client$ npm install –save ws@1.1.1
engine.io-client@1.6.11 /myProject/node_modules/engine.io-client
└─┬ ws@1.1.1
├── options@0.0.6
└── ultron@1.0.2

Using the same method, we can update ws@1.1.0 to ws@1.1.1.

Update negotiator@0.4.9 to negotiator@0.6.1

According to nodesecurity, negotiator@0.6.1 has fixed the denial-of-service vulnerability.
Recall that negotiator@0.4.9 is a dependency of accepts@1.1.4, which is a dependency of engine.io@1.6.11, which in turn is a dependency of socket.io@1.4.8. And we already know that engine.io@1.6.11 is the latest version.

Checking on GitHub – jshttp/accepts, we can see that the current ‘accepts’ version is 1.3.3. Looking in its package.json we can see that it is using ‘negotiator@0.6.1’.This means that we can simply upgrade the ‘accepts’ package:

/myProject/node_modules/engine.io$ cat package.json | grep accepts
“accepts”: “1.1.4”,
/myProject/node_modules/engine.io$ npm install –save accepts@1.3.3
engine.io@1.6.11 /myProject/node_modules/engine.io
└─┬ accepts@1.3.3
├─┬ mime-types@2.1.11
│ └── mime-db@1.23.0
└── negotiator@0.6.1

We are nearly done, but not quite.

The last step is to remove all the extraneous packages that are no longer in use. You can do this by running npm prune from the main project folder:

/myProject$ npm prune
unbuild mime-db@1.12.0
unbuild negotiator@0.4.9
unbuild mime-types@2.0.14
unbuild accepts@1.1.4
unbuild ws@1.1.0
unbuild ultron@1.0.2
unbuild options@0.0.6

Now let’s verify that we indeed no longer have the vulnerable packages by ‘running npm list’ again:

/myProject$ npm list
myproject@1.0.0 /myProject
└─┬ socket.io@1.4.8
├─┬ debug@2.2.0
│ └── ms@0.7.1
├─┬ engine.io@1.6.11
│ ├─┬ accepts@1.3.3
│ │ ├─┬ mime-types@2.1.11
│ │ │ └── mime-db@1.23.0
│ │ └── negotiator@0.6.1
│ ├── base64id@0.1.0
│ ├─┬ engine.io-parser@1.2.4
│ │ ├── after@0.8.1
│ │ ├── arraybuffer.slice@0.0.6
│ │ ├── base64-arraybuffer@0.1.2
│ │ ├── blob@0.0.4
│ │ ├── has-binary@0.1.6
│ │ └── utf8@2.1.0
│ └─┬ ws@1.1.1
│ ├── options@0.0.6
│ └── ultron@1.0.2
├─┬ has-binary@0.1.7
│ └── isarray@0.0.1
├─┬ socket.io-adapter@0.4.0
│ └─┬ socket.io-parser@2.2.2
│ ├── debug@0.7.4
│ └── json3@3.2.6
├─┬ socket.io-client@1.4.8
│ ├── backo2@1.0.2
│ ├── component-bind@1.0.0
│ ├── component-emitter@1.2.0
│ ├─┬ engine.io-client@1.6.11
│ │ ├── component-inherit@0.0.3
│ │ ├── has-cors@1.1.0
│ │ ├── parsejson@0.0.1
│ │ ├── parseqs@0.0.2
│ │ ├─┬ ws@1.1.1
│ │ │ ├── options@0.0.6
│ │ │ └── ultron@1.0.2
│ │ ├── xmlhttprequest-ssl@1.5.1
│ │ └── yeast@0.1.2
│ ├── indexof@0.0.1
│ ├── object-component@0.0.3
│ ├─┬ parseuri@0.0.4
│ │ └─┬ better-assert@1.0.2
│ │ └── callsite@1.0.0
│ └── to-array@0.1.4
└─┬ socket.io-parser@2.2.6
├── benchmark@1.0.0
├── component-emitter@1.1.2
└── json3@3.3.2

As you can see, the packages are updated.

Note that since these changes are done locally, any subsequent npm install will override all of our changes. In order to maintain our changes and force any update procedure to keep our chosen versions for dependencies, we run the shrinkwrap process.

/myProject # npm shrinkwrap
wrote npm-shrinkwrap.json

This file has to be committed to our source code in order to restrict our chosen versioning for the project dependencies.

If we look inside ‘npm-shrinkwrap.json’, we can see the versions for ‘engine.io’ (Showing just the important parts):

“engine.io”: {
“version”: “1.6.11”,
“from”: “engine.io@1.6.11”,
“resolved”: “https://registry.npmjs.org/engine.io/-/engine.io-1.6.11.tgz”,
“dependencies”: {
“accepts”: {
“version”: “1.3.3”,
“from”: “accepts@1.3.3”,
“resolved”: “https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz”
},

“negotiator”: {
“version”: “0.6.1”,
“from”: “negotiator@0.6.1”,
“resolved”: “https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz”
},

“ws”: {
“version”: “1.1.1”,
“from”: “ws@1.1.1”,
“resolved”: “https://registry.npmjs.org/ws/-/ws-1.1.1.tgz”
}
}
}

“engine.io-client”: {
“version”: “1.6.11”,
“from”: “engine.io-client@1.6.11”,
“resolved”: “https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.6.11.tgz”,
“dependencies”: {
“ws”: {
“version”: “1.1.1”,
“from”: “ws@1.1.1”,
“resolved”: “https://registry.npmjs.org/ws/-/ws-1.1.1.tgz”
}
}
}

Verifying Vulnerability Fixes with Twistlock

Now I can run Twistlock scanner again to verify that the vulnerabilities are fixed. In the dashboard view below, you can see that indeed the scanner reported no more vulnerabilities.

image02

Summary

Node modules often carry vulnerabilities, sometimes vulnerable modules are nested several levels deep in the dependency tree. To eradicate vulnerabilities from your node.js application, we must scan for vulnerabilities and find the most efficient way of updating the module or submodules across all the dependencies that it affects

What’s Next?

← Back to All Posts Next Post →