========================
== thoughts of a deer ==
========================
a blog is a blog is a blog is a blog

That one time I installed a rogue npm package

You always hear about it, but never think you will be the one who gets hit. Today it was my turn. Thankfully no harm was caused, but it’s a fun one to tell.

At work I got into the habit of running npm via docker, and of course for just installing dependencies, its a whole lot to remember:

docker run -it --rm -u $(id -u ${USER}):$(id -g ${USER}) -v .:/app --entrypoint npm -w /app node:22-slim install

For this article, let’s pretend it just reads docker run -it --rm node:22-slim install

Naturally I’m calling this via shell alias. I’m actually experimenting with not doing that, as over time I will totally forget what actually gets called here.

So here I was, adjusting the line a bit for clarity via --, and also including --ignore-scripts (as you should):

docker run -it --rm node:22-slim -- install --ignore-scripts

Oops, pwned. Can you spot the mistake?

To my surprise this tainted my git state, and taking a closer look, this actually installed a new dependency.

A package called --ignore-scripts. Yep.

The direct link will result in a 404, which is probably how these survived for 4 years.

But here we can see them in all their glory: https://www.npmjs.com/~realpeha

A screenshot showing published npmjs.com packages with clever names

So what happened here? I thought -- was just a way to indicate the intent of “don’t parse whatever comes next as arguments”.

Well, that’s wrong.

The bash manual states that bash builtins use -- “to signify the end of the options”. In other words, it means “treat everything after this as positional parameters”.

In the case of docker, its implementation stops option parsing at the image name.


The violet part ends up at docker, while the red part is what gets passed on to npm.

npm stops looking at options, and --ignore-scripts gets treated as a package name. Which it just happily accepts, of course. Ironically this could have executed an arbitrary postinstall script.

Now what I intended to do was this:

Which has no side effects and just reads a bit more clearly, considering the number of options that get passed to it (remember that line at the beginning?)

Luckily this package contains no harmful code, at least at this point in time. Its source code consists of less bytes than the command I used to install it, and doubles as an accurate depiction of my thoughts on the javascript ecosystem:

module.exports = '💩'