Sitemap

Before You Install It: How to Check an npm or PyPI Package So It Doesn’t Own You -Hacker’s POV

10 min read4 days ago

--

Press enter or click to view image in full size

A few years ago I ran pip install on a package I found in a random Stack Overflow answer. It worked, the error went away, and I moved on. It never occurred to me that I had just let a stranger's code run on a laptop that held my SSH keys, cloud credentials and a pile of tokens.

Nothing bad happened that time. I got lucky, and luck is not a security strategy.

Most developers treat npm install and pip install like opening a tap. You turn it, water comes out. But every install is you deciding to run code written by someone you have never met, and often code written by someone your dependency's author has never met either. This post is about what to check before you do that, using habits that cost you a few minutes and not a security team.

Why this keeps working for attackers

Attacking a company directly is hard. Attacking the thing that thousands of companies install automatically is much cheaper. A single bad package can land on developer laptops, CI servers and production machines in one afternoon.

Some real cases worth knowing:

  • event-stream (2018). The original maintainer was tired of the project and handed it to a stranger who offered to help. The new maintainer added a dependency called flatmap-stream, which contained code aimed at stealing from a specific Bitcoin wallet app. It sat there for weeks.
  • ua-parser-js (2021). The maintainer’s npm account was hijacked and three versions were published with a crypto miner and a password stealer. This package had millions of weekly downloads.
  • colors and faker (2022). The maintainer deliberately broke his own packages out of frustration. Nobody hacked anything. The trust model just has a human at the end of it.
  • node-ipc (2022). Protestware that wiped files if the machine appeared to be in certain countries. A reminder that “the maintainer is well meaning” can change.
  • Dependency confusion (2021). Researcher Alex Birsan published public packages that matched the names of internal packages used by big companies. Build systems pulled the public version over the private one. It worked on a long list of well known names.
  • PyTorch nightly (late 2022). A package called torchtriton on PyPI shadowed the one meant to come from PyTorch’s own index. People who installed nightly builds got a malicious version that collected system data.
  • The chalk and debug compromise (September 2025). A maintainer was phished with a fake npm support email, and popular packages with billions of combined weekly downloads got malicious versions for a few hours. The payload targeted crypto transactions in the browser.
  • Shai-Hulud (2025). A self spreading npm worm. It stole tokens from infected machines, then used them to publish infected versions of other packages the victim maintained. A second wave followed later that year.

Notice the pattern. Very few of these needed a clever zero day. They needed one tired maintainer, one phished login, one typo, or one install script nobody read.

The four ways a bad package gets to you

Press enter or click to view image in full size

Understanding the route helps you know what to check.

1. Typosquatting. The attacker publishes a package with a name close to a popular one. reqeusts instead of requests. crossenv instead of cross-env. You type fast, you hit enter, you are done. This is also getting a new flavor. AI coding tools sometimes suggest packages that do not exist, and attackers have started registering those hallucinated names, because someone will eventually run the install command the AI gave them. People call it slopsquatting. The fix is the same: never trust a package name just because it appeared in a code suggestion.

2. Dependency confusion. If your company has an internal package called acme-utils and the public registry does not have it, an attacker can publish acme-utils publicly with a higher version number. Depending on how your installer is configured, it may pick the public one.

3. Account takeover. The package is legitimate, the name is right, the history is long, and one day the maintainer’s account is phished or their token leaks. The next version is poisoned. This is the scariest one for normal developers because every normal check looks fine.

4. The maintainer turns, or changes. Project sold, handed over, abandoned, or the person goes rogue. Again, the name looks fine.

What actually runs when you install

This is the part many people have never looked at, and it matters for everything else.

npm. A package can declare preinstall, install and postinstall scripts in its package.json. They run automatically, with your user permissions, during npm install. That includes the scripts of every nested dependency, not just the one you asked for. Most of the malicious npm packages you have read about used this.

PyPI. It depends on the package format. A wheel (.whl) is mostly just unpacked, so nothing executes at install time. A source distribution (.tar.gz) may need to be built, which means setup.py runs on your machine. Also, code in a package's __init__.py runs the moment you import it, and Python processes .pth files in site-packages at interpreter startup, which has been abused as a sneaky place to hide a payload.

So the safest default for Python is to prefer wheels, and the safest default for npm is to not run dependency scripts unless you have decided you need to.

The pre-install check

Press enter or click to view image in full size

Here is the routine. None of it needs paid tools.

1. Check the name, character by character

Copy the install command from the project’s official README or website, not from a blog, a forum answer or an AI chat. Then look at the name again. Look for swapped letters, a missing hyphen, -js or -python tacked on, and extra underscores. For npm, check whether the project publishes under a scope such as @babel/core. The scope is a good sign you are on the real one.

2. Check the history

Look at the package page and ask a few boring questions.

  • How old is the package? A package created last week that claims to be a popular utility is suspicious.
  • How many versions, and how regular? A long boring history is good.
  • Who maintains it, and did that change recently?
  • Downloads are a weak signal. They can be faked or inflated, and a legitimate package can still be compromised. Use them to spot the obviously empty, not to declare something safe.

From the terminal:

npm view some-package time maintainers repository
npm view some-package scripts

For PyPI, the JSON endpoint gives you upload times and file types:

curl -s https://pypi.org/pypi/some-package/json | python3 -m json.tool | less

3. Check the repo, and whether the package matches it

Does the package link to a real repo? Does that repo link back to the package? Is there actual activity, issues, commits from several people? An empty repo with a README and nothing else is a warning.

The subtle bit is that what is on the registry is not guaranteed to match what is on GitHub. The published tarball can contain files that never existed in the repo. Registries now support provenance, which links a published version to the exact build that made it. npm shows a provenance badge when a package is published with it, and PyPI has added attestations. You can check npm signatures and provenance on what you have installed with:

npm audit signatures

Provenance is not a guarantee of safe code. It tells you where it came from, which is useful when you are looking at a version that looks off.

4. Look inside before you install

This is the step almost nobody does, and it takes less than two minutes.

For npm, download the tarball without running anything:

npm pack some-package
tar -tzf some-package-1.2.3.tgz
tar -xzf some-package-1.2.3.tgz
cat package/package.json

You are looking for install scripts, minified or obfuscated files in a package that should not have any, calls to eval, long base64 strings, network requests in code that should not need a network, and anything that reads .npmrc, .ssh, or environment variables.

Get Ishant’s stories in your inbox

Join Medium for free to get updates from this writer.

For Python:

pip download some-package --no-deps --only-binary :all: -d ./inspect
unzip -l ./inspect/*.whl

Open the .whl as a zip, read the top level modules, and search for exec(, eval(, base64, subprocess, urllib, requests.post and any .pth files. If the only option is a source distribution, read setup.py first and be more careful.

If you want a helper to do some of this, GuardDog from Datadog is open source and scans npm and PyPI packages for suspicious patterns. It is a heuristic tool, so treat a clean result as “nothing obvious” and not as a clearance.

5. Don’t run the newest version the minute it appears

Most of the poisoned releases above were caught within hours to days. The cheapest defense in this whole post is just waiting.

Many tools now support a cooldown or minimum release age:

  • pnpm has minimumReleaseAge in its settings.
  • uv has --exclude-newer, which only considers packages published before a given date.
  • Dependabot has a cooldown option, and Renovate has minimumReleaseAge.

Even a few days of cooldown would have avoided most of the incidents where a malicious version was live for a short window. Exception: for a real security patch you may want the fix quickly, so make that a conscious decision and not an automatic one.

6. Lock it down

  • Commit your lockfile and use npm ci in CI, which installs exactly what the lockfile says and fails if it does not match. Do not use npm install in pipelines.
  • Pin versions for anything sensitive. A range like ^1.2.0 means "whatever is newest in this range", which is exactly how a poisoned patch release reaches you overnight.
  • For Python, use hashes. pip-compile --generate-hashes plus pip install --require-hashes -r requirements.txt means pip will refuse anything whose hash does not match. uv's lockfile records hashes by default.
  • Review lockfile changes in pull requests. A dependency bump that adds twelve new transitive packages deserves a look.

Settings worth changing today

npm. Turn off dependency scripts by default:

npm config set ignore-scripts true

Then allow them only for the few packages that genuinely need a build step. pnpm (version 10 and later) already blocks dependency lifecycle scripts by default and has an allow list. If you use npm, expect to run npm rebuild for the packages that really need it.

pip. Prefer wheels. pip install --only-binary :all: some-package refuses to build from source, which means setup.py never runs.

Private packages. To avoid dependency confusion:

  • For npm, use scoped names for internal packages and map the scope to your private registry in .npmrc. Also claim your scope on the public registry so nobody else can.
  • For pip, be careful with --extra-index-url. Pip looks at every index and picks the highest version, so a public package with a bigger version number can win. Prefer a single internal index that proxies the public one, or use a tool like uv that lets you control index priority.

Tools that are actually useful (and free)

  • npm audit and pip-audit. They check for known vulnerabilities. Important caveat: they mostly do not catch fresh malware, because a malicious package is not a "known vulnerability" yet. Use them, but do not feel safe because they are green.
  • OSV-Scanner. Google’s open source scanner that checks your lockfiles against the OSV database, which includes malicious package reports.
  • OpenSSF Scorecard. Gives a repo a health score on practices like branch protection and signed releases. Handy for judging whether a dependency’s maintainers take security seriously.
  • Socket. Has a free tier and flags suspicious behavior in packages such as new install scripts or network access, which is the kind of signal that catches malware before it has a CVE.
  • GuardDog. Mentioned above, good for scanning a package before you add it.

Protect your secrets, because the check will eventually fail

You will not catch everything. So the second layer is assuming something gets through and making sure it finds nothing worth stealing.

Recent attacks like Shai-Hulud went after exactly what is lying around on developer machines: npm tokens, GitHub tokens, cloud keys, and anything in environment variables. So:

  • Do not keep long lived tokens in plain files and shell profiles. Use short lived credentials where you can.
  • Run installs for unfamiliar packages in a container, a dev container or a throwaway VM, not on the laptop that has your production access.
  • In CI, give the install step as few permissions as possible. Do not expose deploy secrets to the job that runs npm install.
  • Use two factor authentication on npm, PyPI and GitHub, and prefer hardware keys or passkeys. If you publish packages, look into trusted publishing, which lets your CI publish without a stored long lived token. Both PyPI and npm support it, and registry rules around tokens and 2FA keep tightening, so check their current docs.
  • Never click an “urgent” email from a registry. The September 2025 incident started with exactly one.

If you think you installed something bad

Don’t just uninstall it and move on. Uninstalling removes the files, not what already ran.

  1. Disconnect the machine or stop the CI runner if you can.
  2. Figure out when it was installed and what ran (check the lockfile history and shell history).
  3. Rotate everything that machine could reach: npm and PyPI tokens, GitHub tokens, SSH keys, cloud credentials, database passwords, .env values.
  4. Check your own published packages and repos for changes you did not make, especially if you are a maintainer. Worm style malware spreads that way.
  5. Rebuild the environment from a clean state instead of cleaning the old one.
  6. Report the package to npm or PyPI so they can take it down and protect others.

My shorthanded keynotes!

If you read nothing else, do these five things:

  1. Copy install commands from the official source and check the spelling.
  2. Look at install scripts before you run them, and disable dependency scripts by default.
  3. Use lockfiles, pin versions, and use hashes for Python.
  4. Add a cooldown so brand new releases do not hit you the same day.
  5. Keep secrets out of reach of your install step.

None of this makes you immune. The registries are huge, open systems, and that is exactly why they are useful. But most attacks succeed because nobody looked, and a developer who spends two minutes looking is already harder to hit than most.

If this helped, share it with the teammate who runs npm install on everything. And if you have a story about a package that almost got you, I would like to hear it in the comments.

#Cybersecurity #SoftwareDevelopment #JavaScript #Python #Programming

#Ethicalhacking #webdevelopment #Deployment

--

--