Compliance & SecOps

JavaScript Dependency Security: Managing Vulnerabilities with npm, Bun, and pnpm

An operational engineering guide to lockfiles, audits, safe upgrades, overrides, and CI enforcement for JavaScript projects using npm, Bun, or pnpm.

·7 min read·
#AppSecurity#Dependencies#npm#Bun#pnpm#Lockfiles#CI

Short answer

Use one package manager per repository, commit its lockfile, pin newly approved direct dependencies, and run that manager's audit in CI. Fix findings with the smallest reviewed update or temporary override, then verify the lockfile, tests, and production build before merging.

TL;DR

  • package.json is only the visible layer — most of your code is transitive dependencies.
  • Commit the lockfile (package-lock.json, bun.lock, or pnpm-lock.yaml) and use frozen installs in CI.
  • Use one package manager per repository — do not mix npm, bun, and pnpm lockfiles.
  • Run audit on every pull request. Fail CI on high and critical findings.
  • Prefer the narrowest safe fix: upgrade the direct dependency, then compatible updates, then a documented override. Avoid --force and blanket --latest upgrades in CI.
  • Every security patch is complete only when audit, test, and production build all pass.

The problem: package.json is only the visible layer

A project may declare 40 packages in package.json, but installing them can bring in hundreds of transitive dependencies.

Your application
└── framework
    └── build plugin
        └── parser
            └── utility library
                └── vulnerable package

Your code may never import the vulnerable package directly. It can still run during development, testing, CI, deployment, or production.

This creates three important facts:

  1. Direct dependencies are not the complete dependency tree.
  2. Development dependencies can still be security-sensitive.
  3. The lockfile is as important as package.json.

The lockfile records the exact resolved versions installed by the project.

Package managerLockfile
npmpackage-lock.json
Bunbun.lock
pnpmpnpm-lock.yaml

A lockfile should be committed to version control and reviewed in pull requests. Bun explicitly recommends committing its bun.lock file. Bun lockfile documentation

Why dependency hygiene matters

A clean dependency process protects more than production code.

Production vulnerabilities

Packages may process requests, parse files, make network calls, generate HTML, handle authentication, or access databases. Vulnerabilities in those packages can lead to:

  • Remote code execution
  • Denial of service
  • Path traversal
  • Information disclosure
  • Cross-site scripting
  • Server-side request forgery
  • Prototype pollution
  • Authentication bypasses

Not every advisory is exploitable in every application. A YAML parser vulnerability matters much more if users can upload YAML than if the parser only runs during local linting. But every finding should be assessed, not ignored.

CI and build compromise

Development dependencies are not harmless.

A package may execute while running:

npm install
bun install
pnpm install
npm run build
bun run build
pnpm run build

A compromised build tool or install script may gain access to:

  • Repository source code
  • Environment variables
  • CI secrets
  • Cloud credentials
  • Package publishing tokens
  • Deployment credentials

Treat your CI dependency tree as part of your production security boundary.

Unreliable deployments

Without a committed lockfile, two developers may install different versions from the same version range.

{
  "dependencies": {
    "example-package": "^2.1.0"
  }
}

The ^ range permits compatible updates. One developer may get 2.1.2, another 2.4.0, and a later deployment may resolve something else.

This creates dependency drift:

Same repository
      ↓
Different package resolution
      ↓
Different local behavior
      ↓
Unexpected CI or production behavior

A lockfile gives the team a reviewed, reproducible dependency graph.

The core rule: use one package manager per repository

Choose one package manager per repository. Commit only its matching lockfile. Use that package manager in local development and CI.

Repository typeLocal installCI installAudit
npmnpm installnpm cinpm audit
Bunbun installbun cibun audit
pnpmpnpm installpnpm install --frozen-lockfilepnpm audit

Do not casually mix package managers.

For example, a project using Bun should not introduce package-lock.json merely to run npm audit. It can cause a different resolution tree and unrelated lockfile changes. Bun has its own lockfile and audit command. Bun audit documentation

A practical dependency-security workflow

Use this five-step loop:

Discover → Assess → Remediate → Verify → Prevent recurrence

1. Discover vulnerabilities

Run the audit command for the package manager that owns the repository.

# npm
npm audit

# Bun
bun audit

# pnpm
pnpm audit

For CI, select a threshold that should fail the build:

# npm
npm audit --audit-level=high

# Bun
bun audit --audit-level=high

# pnpm
pnpm audit --audit-level=high

An audit should identify:

  • Vulnerable package
  • Installed version
  • Severity
  • Advisory or CVE
  • Dependency path
  • Recommended fix, if one exists

Example:

js-yaml
  application → framework → build-plugin → js-yaml

  high: Crafted YAML may cause CPU exhaustion

The dependency path is important. It identifies which top-level dependency introduced the vulnerability and helps determine the safest remediation.

2. Assess the actual impact

Before upgrading, answer:

1. Is this dependency in production, development, CI, or all three?
2. Is the vulnerable functionality exposed to untrusted input?
3. Is a patched version available?
4. Can the direct parent package be updated safely?
5. Does the fix require a major-version change?
6. Is a temporary mitigation available?

This prevents two opposite mistakes:

  • Treating every audit message as equally urgent.
  • Ignoring all development dependency findings.

A high-severity vulnerability in a build tool may not be internet-facing, but it can still be dangerous if it runs with CI secrets.

3. Apply the narrowest safe fix

Use this order:

  1. Upgrade the direct dependency that introduces the vulnerable package.
  2. Apply a compatible package-manager update.
  3. Add a temporary override for a patched transitive package.
  4. Replace or remove the parent dependency if no safe patch exists.

Avoid broad, breaking upgrades as an automatic response:

# Do not run automatically in CI
npm audit fix --force
bun update --latest
pnpm update --latest

Instead, inspect the proposed changes first.

npm audit fix --dry-run --json

Then upgrade the smallest possible version range that resolves the issue.

4. Use overrides for transitive vulnerabilities

Sometimes the parent package has not released an update, but the vulnerable transitive dependency already has a safe patch.

For npm and Bun:

{
  "overrides": {
    "vulnerable-transitive-package": "x.y.z"
  }
}

For pnpm:

{
  "pnpm": {
    "overrides": {
      "vulnerable-transitive-package": "x.y.z"
    }
  }
}

Example:

{
  "overrides": {
    "@babel/core": "7.29.6",
    "brace-expansion": "5.0.7",
    "js-yaml": "4.3.0"
  }
}

Then regenerate the lockfile:

# npm
npm install

# Bun
bun install

# pnpm
pnpm install

Bun supports npm-style top-level overrides for transitive dependencies. Bun overrides documentation

Overrides should be documented. Every override should include:

- Advisory/CVE reference
- Vulnerable version
- Patched version
- Parent dependency that introduced it
- Reason an override is safe
- Owner
- Planned removal date

An override is a deliberate temporary control — not permanent dependency clutter.

5. Verify before merging

After any package security change, run:

# Audit
<package-manager> audit

# Tests
<package-manager> test

# Production build
<package-manager> run build

# Inspect dependency changes
git diff -- package.json <lockfile>

For example, verify the result with the package manager that owns the repository:

# npm
npm audit
npm test
npm run build
git diff -- package.json package-lock.json

# Bun
bun audit
bun test
bun run build
git diff -- package.json bun.lock

# pnpm
pnpm audit
pnpm test
pnpm run build
git diff -- package.json pnpm-lock.yaml

A security patch is complete only when the audit passes and the application still builds and behaves correctly.

Use reproducible installs in CI

CI should never silently resolve fresh dependency versions.

Use lockfile-enforced installs:

# npm
npm ci

# Bun
bun ci

# pnpm
pnpm install --frozen-lockfile

Bun documents bun ci as an install that enforces lockfile consistency. Bun install documentation

A baseline CI workflow:

# Install exactly the reviewed dependency tree
<package-manager frozen install>

# Fail on high and critical vulnerabilities
<package-manager> audit --audit-level=high

# Verify application behavior
<package-manager> test
<package-manager> run build

For npm repositories, also consider:

npm audit signatures

This checks registry signatures and available provenance information for installed packages. npm signature verification documentation

Benefits of clean dependency hygiene

Reproducible builds

A committed lockfile ensures every developer, CI runner, and deployment uses the same reviewed versions.

Faster incident response

When a new advisory appears, a clean dependency process makes it easy to answer:

Is the package installed?
Which version is deployed?
Which dependency introduced it?
Does it run in production?
Is a patch available?
Can we safely override it?

Smaller blast radius

Minimal dependencies reduce the possible impact of a compromised package or vulnerable transitive dependency.

Better engineering ownership

A clean process replaces "the package manager installed it" with a clear decision trail:

Dependency added
        ↓
Reason documented
        ↓
Lockfile reviewed
        ↓
Audit passed
        ↓
Tests and build passed
        ↓
Owner assigned

Closing thought

Dependency management is not maintenance work that happens after development. It is part of secure development. A repeatable audit-and-override loop, one package manager per repo, and frozen installs in CI cover most of the avoidable risk in a JavaScript codebase.

For the AI-coding side of the same problem — hallucinated packages, slopsquatting, and safe review of AI-generated install commands — see the companion post: Supply-Chain Security in the AI Coding Era.

Public profile lookup

Ask AI About the Author

Open this query in ChatGPT, Claude, or Perplexity.

Comments

Comments are open to confirmed email subscribers. Use the email you subscribed with. To edit a comment, delete it and post a new one.

0/2000
Verify:

    Subscribe to get the new blogs.

    Field notes from someone who ships before they write about it. Sovereign AI, AI-SDLC, DevOps, and what 59 production deployments teach you. No spam. Unsubscribe anytime.

    Related field notes