4 min read

Quick npm and pip Security Hardening Tweaks for Supply Chain Attacks

# security# npm# python# devops# supply-chain

After the recent wave of supply chain attacks, I changed one boring default on my servers and dev machines:

Package installs should not run arbitrary setup code unless I explicitly ask for it.

This is not theoretical anymore. The XZ backdoor showed how much damage a trusted upstream package can do. tj-actions/changed-files showed how quickly CI secrets can leak. The Axios npm compromise used a dependency with a postinstall payload. Microsoft’s Mini Shai-Hulud write-up is another reminder that attackers are targeting CI/CD credentials directly.

So here is the quick hardening pass I now want on machines that run npm install or pip install.

npm: disable install scripts and delay new packages

Put this in your user npm config:

npm config set --location=user ignore-scripts true
npm config set --location=user allow-git none
npm config set --location=user min-release-age 7
npm config set --location=user audit true
npm config set --location=user strict-ssl true
npm config set --location=user save-exact true

The two important ones:

  • ignore-scripts=true stops npm lifecycle scripts such as preinstall, install, and postinstall from running automatically.
  • min-release-age=7 tells npm to avoid package versions published less than seven days ago.

The age gate is not magic. A malicious package can wait. But a lot of registry attacks get noticed in the first hours or days, and I do not want my servers or CI runners to be in the first wave.

You need a recent npm for min-release-age. I moved my server to Node 24.16.0 through nvm, which bundled npm 11.13.0. I did not upgrade npm separately.

pip: wheel-only, hashes, and the same delay

pip is different from npm. It does not have the same general postinstall lifecycle model, but source builds can still execute package build code. So the safer default is: wheels only, hashes required, and no fresh uploads.

python3 -m pip config --user set install.only-binary ':all:'
python3 -m pip config --user set install.require-hashes true
python3 -m pip config --user set install.uploaded-prior-to P7D
python3 -m pip config --user set global.no-input true

This makes casual installs fail. That is intentional.

If I type:

python3 -m pip install boltons==25.0.0

I want pip to complain that hashes are missing. Server installs should come from a pinned, hashed requirements file, not from whatever PyPI returns right now.

pip documents both hash-checking mode and uploaded-prior-to.

Change install habits too

Config helps, but habits matter.

For Node projects, prefer:

npm ci

over:

npm install

Keep package-lock.json committed.

For Python projects, install from a locked file with hashes:

python3 -m pip install -r requirements.lock.txt

If a project cannot do this yet, that is useful information. It means the install path still depends on trust and timing.

Keep an explicit exception path

Some packages really do need install scripts. Native modules compile things. Some tools download platform binaries. Some repos use prepare to install Git hooks.

Fine. Make that explicit.

Run trusted setup commands yourself:

npm run prepare

Or build native dependencies in a disposable environment with no secrets:

  • no SSH agent
  • no npm or PyPI token
  • no cloud credentials
  • no production .env
  • limited outbound network

Then copy the artifact into the real environment.

The point is not “never run setup code.” The point is “do not let every transitive dependency run setup code by default.”

Where I would start

Start with servers and CI:

  1. Disable npm install scripts.
  2. Add the seven-day npm release-age gate.
  3. Make pip wheel-only, hash-required, and seven-day delayed.
  4. Use npm ci and locked Python requirements.
  5. Move exceptions into a no-secrets sandbox.

This does not solve every supply chain problem. It will not catch malicious code that runs when you import a package. It will not save you from every compromised maintainer account.

But it removes one dangerous default:

Installing dependencies should not automatically execute code on machines full of secrets.

That is a small change with a lot of upside.

References

Share