Back to Blog

Secure Node Development

Published on: 2025-2-12 Secure Node Development

The supply chain attacks are all the rage these days. These cyber attacks leverage various software packages and repositories of packages to deliver malware.

For those interested in tech news, names like:

  • typo squatting,
  • package hijacking,
  • domain squatting,
  • etc.

will sound awfully familiar.

Malware often aims to hijack production systems, but in recent years, there is a worrying trend of targeting developers.

And it's no surprise.

Most developers have secrets, keys, and passwords stored on their machines in their local environment to effectively work with development and production systems.

Those crypto criminals just love to sniff out your cloud access keys and commandeer your servers for crypto mining. Or, they might spin out an impromptu bot farm.

These attacks can happen in most (if not all) repositories for Go, Python, and others. Even Linux repositories have been recently infiltrated.

In this article, I will focus on JavaScript and NPM. JavaScript is by far the most popular programming language on our planet.

And JavaScript engineers tend to use many third-party packages, from which they can choose.

This makes the JavaScript ecosystem a prime target for abuse.

Luckily, setting up core defenses is relatively easy.

Built-In Defense

Your npm, pnpm, or yarn tool for managing JavaScript packages has a first level of supply chain attack prevention.

Each time you install a package, it is added to package.json and package-lock.json. The latter file contains a cryptographic signature of the package you installed.

$ npm install wideangle-nuxt

will result in the following entry:

"node_modules/wideangle-nuxt": {
  "version": "2.0.0",
  "resolved": "https://registry.npmjs.org/wideangle-nuxt/-/wideangle-nuxt-2.0.0.tgz",
  "integrity": "sha512-mEe3Ysdkk1zhhasQCC4aZnfCeDmy/Ohtv6xIhPEPBCWOaSKyt8EWPHUeb8Gt6dew0LE/371PYHJp6avRFjQDsw==",
  "license": "Apache-2.0",
  "dependencies": {
    "@nuxt/kit": "^3.15.3",
    "defu": "^6.1.4",
    "wideangle-vuejs": "2.0.0"
  }
},

In case a bad actor tampers with the package's remote repository, you will quickly receive a notification, and the malware-ridden package won't be used in your project.

NPM, the most popular online JavaScript package repository, has also improved its security. For instance, uploading packages requires authentication, which can be secured with 2FA. Republishing the same or an older version can only be done within the first 24 hours, allowing mistakes to be quickly fixed while preventing tampering with older versions in use.

These integrity checks and npmjs.com's security mean you should not have a previously installed good package sneakily replaced by malware.

So, what about future releases and updates? Well, that's not so easy.

Future-Proofing Your Defenses

If a developer's Git or NPM account is hijacked, or their authentication token is leaked, these can be abused to push new, updated packages with extra malicious content.

In January 2025, approximately 299,395,846,617 packages were downloaded from NPM. Having an infected package online for even an hour, if it's a popular package (or worse, a dependency of a few other popular packages), it spreads like wildfire.

So, what can you do?

Containerize it!

What I like to do is always run and develop Node projects in a Docker container that runs Node as a non-privileged account.

Step 1: Prevent Running 'install' Outside the Container

First, we want to ensure that we don't accidentally execute npm install outside the container.

We can achieve this by defining a preinstall script in package.json:

"scripts": {
  "preinstall": "[ \"$IS_CONTAINER\"!= 1 ] && echo \"FORBIDDEN\" 1>&2 && exit 1; exit 0",
  "build": "nuxt build",
  "dev": "nuxt dev",
  "generate": "nuxt generate",
  "preview": "nuxt preview",
  "postinstall": "nuxt prepare"
},

Attempting to run npm install yields:

$ npm install

> preinstall
> [ "$IS_CONTAINER"!= 1 ] && echo "FORBIDDEN" 1>&2 && exit 1; exit 0


FORBIDDEN
**npm** error code 1
**npm** error path /Users/jarek/workspace/project
**npm** error command failed
**npm** error command sh -c [ "$IS_CONTAINER"!= 1 ] && echo "FORBIDDEN" 1>&2 && exit 1; exit 0

Step 2: Create a Development-Only Docker Compose File

Now that we've stopped potentially malicious files from running directly on our workstation by keeping them in a container, how can we actually do anything?

To accomplish this, we can use Docker again. Docker Compose is a handy tool that can run necessary commands easily.

I like to define services such as install, build, audit, and fix. An example Docker Compose file, docker-compose.yaml, can look like this:

services:
  base:
    image: node:20-alpine
    environment:
      IS_CONTAINER: 1
    volumes:
      -.:/opt/app
    working_dir: /opt/app
  install:
    extends:
      service: base
    command: npm install
  build:
    extends:
      service: base
    command: npm run build
  dev:
    extends:
      service: base
    command: npm run dev
  audit:
    extends:
      service: base
    command: npm audit fix
  fix:
    extends:
      service: base
    command: npm audit fix --force

Now, say you want to install a new package you added to package.json. No problem. Simply run:

$ docker compose run install

The file will end up being placed in your ./node_modules, but none of the post-installation scripts will trigger on your account and will remain contained by Docker.

Likewise, should you launch an application for development, any token exfiltration script running from a malicious package won't have access to your AWS secret key or other precious tokens.

Are We Done?

So, is that it? Are we done with security?

Nope. Not even close. But these two mechanisms can go a long way in making sure your secrets are safe.

Combine this vigilance with checking packages' reputations and monitoring tech news for new outbreaks, and you have a decent chance of not getting infected.

As always, stay safe.

Looking for web analytics that do not require Cookie Banner and avoid Adblockers?
Try Wide Angle Analytics!