Dependency risk: the code you ship but never wrote
A modest application ships a few hundred packages and a few thousand transitive ones. Almost none were read by anyone on the team, most execute code at install time, and each maintainer is one compromised account away from running arbitrary commands in your build. The exposure is unavoidable; being unable to see or reduce it is not.
Application security attention goes overwhelmingly to code the team wrote, which is a small minority of what actually runs. The dependency tree is the larger surface, and it has a property first-party code does not: it can change underneath you without anyone opening a pull request.
How dependency compromise actually happens
Note that only two of those are about a vulnerability in the conventional sense. The rest are about someone else's code changing, which is why vulnerability scanning alone is an incomplete answer.
The lockfile is the primary control
A committed lockfile with integrity hashes is the single highest-value measure available, and it is free. It pins the exact version of every direct and transitive dependency, and records a hash so a substituted artefact fails to install rather than installing quietly.
Install scripts are the sharpest edge
A package can run arbitrary commands during installation, with the privileges of whoever ran the install. This is the mechanism most real-world attacks have used, because it executes before any of your own code, in an environment that usually holds registry tokens, cloud credentials and the source tree.
- Audit which packages run install scripts. The list is usually shorter than expected and worth knowing by name.
- Disable them where you can. Native modules need them; most packages do not, and prebuilt binaries frequently remove the need.
- Give CI no credentials it does not need for that job. An install script can only exfiltrate what is present in the environment.
- Treat a developer machine as production for this purpose. It holds credentials for everything, and installs run there constantly.
Most advisories do not apply to you
An audit reporting forty vulnerabilities tends to produce either panic or, more commonly, permanent indifference. Both are avoidable, because the relevant question is not whether a flaw exists in a package you ship but whether your code can reach it.
The output of triage should be a short list that genuinely matters and a written note for each dismissal. The note is what stops the same advisory being re-triaged every month, and what lets someone else check the reasoning rather than repeat the work.
The build pipeline is part of the supply chain
Attention focuses on the packages and skips the machinery that installs them. A compromised CI step is strictly more powerful than a compromised dependency, because it holds the credentials and produces the artefact that ships.
- Third-party actions and base images pinned by digest — a tag is a mutable pointer
- Least-privilege tokens per job, and no long-lived registry credential where OIDC federation is available
- Secrets exposed only to the jobs that need them, not to every step in the workflow
- Fork pull requests denied access to secrets entirely, since their code runs in your pipeline
The control nobody applies: fewer dependencies
Every measure above manages exposure. Only one reduces it. A dependency added for a single function carries its entire transitive tree, its maintainer's account security and its install scripts — in exchange for code you could often have written in a dozen lines.
- Before adding a package, check what it pulls in transitively rather than only what it does
- Prefer a platform API where one now exists; many popular packages predate their standard equivalents
- Audit periodically for dependencies with one call site, which are usually the easiest to remove
- Treat an unmaintained dependency as a scheduled problem, not a stable one — no maintainer means no fix when a flaw is disclosed
This is the same economics as caching: managing the cost is useful, and not incurring it is better. A tree you can reason about is worth more than a tree you can scan.
Questions
Should the lockfile be committed?
Always. Without it, a version range like ^4.2.0 authorises every future minor release to run in your build, including one published from a compromised maintainer account. The committed lockfile pins the exact resolved version of every direct and transitive dependency and records an integrity hash, so a substituted artefact fails to install rather than installing silently. Use npm ci in CI, never npm install.
How should we handle forty audit findings?
Triage rather than either panic or ignore. Ask whether each one is in a production dependency or only a build tool, whether your code actually calls the affected function, whether untrusted input can reach the vulnerable path, and whether your configuration matches the advisory's prerequisite. Fix what survives, and write down the reasoning for each dismissal so the same advisory is not re-triaged every month.
What is the most dangerous part of installing a package?
The install script. Packages can execute arbitrary commands during installation, with the privileges of whoever ran the install, before any of your code runs — in an environment that typically holds registry tokens, cloud credentials and the source tree. Audit which dependencies use them, disable them where they are not required, and keep credentials out of jobs that do not need them.
Why pin CI actions by digest instead of version tag?
A tag is a mutable pointer: whoever controls the repository can move v5 to different code, and your pipeline will run it on the next build with no change on your side. A commit digest is immutable. Since a compromised CI step holds your credentials and produces the artefact you ship, it is a higher-value target than any individual dependency.