Skip to content
Security6 min read

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.

Part ofCybersecurity & Information Security

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

PathMechanismPrimary control
Maintainer account takeoverA legitimate package publishes a malicious versionLockfile, delayed adoption, provenance
TyposquattingA near-identical name is installed by mistakeReview what gets added; check download counts
Dependency confusionA public package shadows an internal nameScoped names, registry precedence config
Malicious transitive updateA dependency of a dependency changesLockfile committed, full-tree review
Build-tool compromiseThe CI action or image is maliciousPin by digest, least-privilege tokens
AbandonmentNo maintainer to fix a disclosed flawTrack maintenance status, plan replacement

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.

text
# In CI, always. Installs exactly the lockfile, fails if package.json
# and the lockfile disagree, and never silently resolves a new version.
npm ci

# Never in CI. This is allowed to update the lockfile, which means the
# build can install something no one reviewed.
npm install

# Verify what the lockfile actually permits: any entry without an
# integrity hash is an unpinned artefact.
npm ls --all --json > tree.json

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.

  1. Audit which packages run install scripts. The list is usually shorter than expected and worth knowing by name.
  2. Disable them where you can. Native modules need them; most packages do not, and prebuilt binaries frequently remove the need.
  3. Give CI no credentials it does not need for that job. An install script can only exfiltrate what is present in the environment.
  4. 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.

AskIf no
Is it in a production dependency, or only a build tool?A build-tool flaw is not exposed to visitors — still fix it, but not tonight
Does our code call the affected function?Unreachable code is not an exposure; record the reasoning
Is the vulnerable path reachable by untrusted input?A parser flaw only matters if attackers control what is parsed
Does the environment match the prerequisite?Many advisories require a specific configuration you may not use

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.

yaml
permissions:
  contents: read          # deny by default, grant per job

jobs:
  build:
    steps:
      # Pinned by commit digest, not by tag. A tag can be moved to
      # point at different code; a digest cannot.
      - uses: actions/checkout@8f4b7f8  # v5.0.1
      - uses: actions/setup-node@1e60f6  # v5.0.0
        with:
          node-version: "24"
          cache: npm

      # Exact lockfile install, and no install scripts unless a
      # dependency genuinely requires one.
      - run: npm ci --ignore-scripts
      - run: npm audit --audit-level=high
      - run: npm run verify
  • 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.

Want this applied to your systems?

Most of what is described here begins as an assessment: a fixed-scope review producing a prioritised written report you can act on with any supplier.

Prefer email? hello@kornspire.com