Most of your application is somebody else’s
A small web project pulls in hundreds to low thousands of packages once transitive dependencies are counted. Every one of them runs with your application’s permissions, in your process, with access to your environment variables, which is where your database password and your API keys live.
The threat is not theoretical and it is not only about old versions with known bugs. There is a second, sharper category: packages that are compromised on purpose, published deliberately to be installed.
The four ways this actually happens
1. A known vulnerability in an old version. The ordinary case, and the one scanners find. Fixed by updating.
2. A typosquat. A package named one character away from a popular one (a hyphen moved, a letter doubled) published with an install script that reads your environment and posts it somewhere. These appear in the thousands on the large registries and are usually removed within days, which is plenty of time.
3. A maintainer account taken over. A popular package gets a new version containing something extra. This has happened repeatedly to widely used JavaScript packages, and the malicious version is often live for hours before anyone notices: hours in which every unpinned build pulls it.
4. A long social attack. The most sobering example remains the xz backdoor found in 2024: a contributor built trust in a small, critical compression library over roughly two years, became a maintainer, and inserted a backdoor targeting SSH that shipped into pre-release distributions. It was caught by an engineer investigating a 500-millisecond delay in a login benchmark. No scanner found it, and nothing about the maintainer’s behaviour looked wrong until it did.
Draw the honest conclusion: tools reduce the first category well, the second and third partly, and the fourth not at all. What protects you against the fourth is a smaller dependency count and a build that cannot silently take new code.
The controls that are worth the effort
- Commit the lockfile and install from it.
npm ci,pip install -r requirements.txtwith hashes,uv sync --frozen,bundle install --deployment. A build that resolves versions freshly can pull a package published ten minutes ago. - Delay adoption. A cooling-off period (do not install any version less than a few days old) removes most of the window in which a compromised release is live. Some registries and tools support this directly; a scheduled update job that only accepts versions older than a week gets you the same thing.
- Turn off install scripts where you can. Much registry malware runs in a package’s post-install hook.
npm ci --ignore-scriptsbreaks some packages and is worth trying; know which of your dependencies need a script to run. - Pin CI actions by commit hash, not by tag. A tag can be moved. In 2025 a widely used GitHub Action was modified to dump secrets from build logs, affecting thousands of repositories, and hash-pinned users were unaffected.
- Read what you add. For a small package, open the source before you depend on it. The number of dependencies whose entire content is thirty lines you could have written is larger than people admit.
- Prefer fewer, larger, better-maintained dependencies to many small ones. Every package is a maintainer you are trusting, and a package with one maintainer and no funding is a risk regardless of anybody’s intentions.
Scanners, and the noise problem
npm audit, pip-audit, Trivy, Grype and Dependabot are free and worth running in CI. They will also produce a lot of findings that do not matter: a vulnerability in a code path you never call, a denial-of-service issue in a build-time tool, a critical rating for something exploitable only with local access.
Unactioned alerts train people to ignore alerts, which is the same failure as noisy paging. Triage on two questions: does the vulnerable code run in production, and can untrusted input reach it. Fix what passes both immediately, batch the rest into a monthly update, and write down the ones you deliberately accepted with a date to revisit. A short honest list beats a long ignored one.
Where your secrets sit during all this
Build systems are attractive targets precisely because they hold credentials with the power to publish. Give CI the narrowest tokens that work, scope them per repository, prefer short-lived cloud credentials over long-lived keys, and never expose deployment secrets to a workflow triggered by a pull request from a fork. That last one has been the entry point in several public incidents.
The five-minute version
Commit the lockfile, install with the frozen command, pin your CI actions by hash, run one scanner weekly, and update deliberately on a schedule instead of reactively at midnight. That is most of the available protection, and it costs a morning to set up once.
Every lesson here is open. Your progress is recorded once you pass the Agents and Automation exam.
Check your understanding
A team pins every dependency version exactly in package.json but runs npm install in CI without committing the lockfile. Which risk remains open?
Pick the one you would defend. Your answer stays private.
Stuck on something?Ask Tri, your AI tutor
