Recent NPM supply chain attacks are a reminder that the use of package managers is a real danger. But it's just not avoidable, building without package references is not practical.
This is not just a TypeScript problem. Kotlin, C#, Python, and others all have the same fundamental issue: modern software routinely imports code from hundreds or thousands of strangers during install and build. That should make everyone a bit uncomfortable.
The arrival of AI empowered attackers is magnifying the problem.
Most modern package attacks are temporary:
The attack may only exist for a few hours or days. So one strategy is to try to not be in that early adopter group that's vulnerable. But a balance also has to be found because we don't want to wait too long and miss out on a valid security patch.
The industry is now converging on a few sensible ideas:
The single highest ROI defense is package release cooldowns.
PNPM implemented this early:
|
|
The package manager refuses to install versions published too recently. You can also exempt trusted packages to keep up with the latest versions of these.
This idea is spreading quickly to other package managers and language ecosystems.
Like anything, this change isn't without it's cost. It can be a maintenance issue when you know a newer valid version is required, but it's still within the cooldown period. If your solution can't wait it out, then you may have to setup an exclusion, but then remember to remove that exclusion later.
PNPM still has the strongest overall defense-in-depth model right now. Especially since version 11 started enabling some protections by default. And they are actively improving the situation.
One PNPM feature I particularly like that is included by default in version 11:
|
//// pnpm-workspace.yaml
blockExoticSubdeps: true |
This blocks transitive dependencies from resolving through:
Instead of:
| "some_dependency": "github:user/project" |
or:
| "some_dependency": "https://some-random-site/package.tgz" |
everything must come from trusted registries.
Attackers increasingly hide payloads in unusual dependency paths because:
Repositories hosted on GitHub have a feature available called Dependabot which scans your project for outdated dependencies. When found, it will create a pull request suggesting the upgrade. This is cool, but it's another way an un-ripened package may get into your project. It is configurable to specify a cooldown period there as well.
Your package references include the version being relied on. Often this will include version wildcards saying you will accept more recent patch, minor, or major upgrades automatically. This has a couple problems:
You can mitigate this by removing or minimizing those wildcards to prevent the automatic updates. You should also check your package lock-files into source control to ensure everyone is working off of the same version. This doesn't come for free since you now have to invest time in doing those upgrades manually.
But this leads to the next topic:
Don't blindly accept package updates. Go to the package repository and see what changed. Dig into the GitHub source and compare the code changes. You'll often find nice surprises there like new features or improvements you can take advantage of.
AI is good for analyzing the cryptic maze of dependencies, so you can ask it to check for potential version incompatibilities or give you a list of changes.
The NPM ecosystem normalized install-time code execution years ago and everyone just accepted it. It should never have become normal. The way this works is that when you download a package, some code runs on your machine to set it up, maybe compiling source for your particular operating system. But this is opening the door for anything to be run silently on your machine.
A huge number of supply chain attacks rely on these package manager scripts:
The safest option is still:
| ignore-scripts=true |
But that is often too aggressive for real projects and could cause some tools to stop working. PNPM has a better option now to allow white-listing of valid scripts that are essential:
|
allowBuilds: esbuild: true sharp: true |
The implementation details differ, but the same principles are starting to appear almost everywhere. Where the package manager doesn't directly have this support, you can ask your bot to help you out, prompting it to find potential upgrades over a given cooldown age.
Now is a great time to consider upgrading from NPM to PNPM! While BUN is also a great choice, its a bit heavier of a lift.
In addition to the enhanced security, PNPM installs packages in a central location on your machine, and creating symlinks in your app specific packages folder. This means MUCH faster installs of common packages, and MUCH less wasted disk space especially for monorepos.
|
# powershell / bash
npm install -g pnpm rm -rf node_modules rm package-lock.json pnpm install
# ToDo: Apply the configuration suggestions from above to lock things down |
Recent npm supply chain attacks video: https://www.youtube.com/watch?v=D13Q4Z7-4Oo
Matija Grcic dependency cooldown reference: https://github.com/matijagrcic/dependency-cooldowns
pnpm supply chain security: https://pnpm.io/supply-chain-security
Dependabot cooldown documentation: https://github.blog/changelog/2025-07-01-dependabot-supports-configuration-of-a-minimum-package-age/
#AiBetter