Skip to main content
Back to blog
April 11, 2026·Updated May 2, 2026|13 min read|Antoine Duno|Devops Security

Dependency Scanning: How to Keep Your npm Packages Secure in 2026

Most security breaches involving npm packages are not zero-days — they are known vulnerabilities that sat in package.json for months while teams deferred updating. This guide covers how CVEs get into your dependencies, how to scan for them effectively, how to keep them out with automation, and how to triage the noise without ignoring the signal.

Antoine Duno

1,987 words

AD

Antoine Duno

Founder of ZeriFlow · 10 years fullstack engineering · About the author

Key Takeaways

  • Most security breaches involving npm packages are not zero-days — they are known vulnerabilities that sat in package.json for months while teams deferred updating. This guide covers how CVEs get into your dependencies, how to scan for them effectively, how to keep them out with automation, and how to triage the noise without ignoring the signal.
  • Includes copy-paste code examples and step-by-step instructions.
  • Free automated scan available to verify your implementation.

Dependency Scanning: How to Keep Your npm Packages Secure in 2026

The average Node.js application has over 1,000 transitive dependencies. You wrote maybe 200 lines of package.json. The rest arrived through the dependency tree. You did not review those 50,000 files before they became part of your application, and neither did anyone else on your team.

This is the supply chain problem. You trust npm packages the same way you trust shared libraries in any ecosystem, but npm''s ecosystem is unusually large, unusually flat (many tiny single-purpose packages), and historically has had some high-profile compromises — event-stream, ua-parser-js, node-ipc, and others.

Dependency scanning is not optional anymore. It is a baseline hygiene practice in the same category as having tests and a linter.


How CVEs Get Into Your Dependencies

Understanding the entry points helps you build effective defenses.

Direct dependency vulnerabilities

A package you directly npm installed has a vulnerability disclosed. Example: your application uses lodash@4.17.4, and CVE-2021-23337 is disclosed — a prototype pollution vulnerability in lodash.set. You are directly affected.

This is the easiest case. Your package.json references lodash directly. The fix is npm install lodash@latest.

Transitive dependency vulnerabilities

A package you depend on has a dependency with a vulnerability. Your package.json does not mention this package at all — it is three levels deep in the dependency tree.

your-app
  └── express@4.18.2
        └── qs@6.11.0        ← vulnerable package, not in your package.json

You cannot fix this by editing your package.json. You need to either: 1. Update express (hoping the new version depends on a patched qs) 2. Use npm install qs@latest --save-dev to force a specific version (via resolutions field) 3. Add a overrides entry in package.json:

json
{
  "overrides": {
    "qs": "^6.12.0"
  }
}

Malicious packages (supply chain attacks)

A package maintainer''s npm account is compromised, and a malicious version is published. Or a package with a name similar to a popular one is published hoping developers mistype it (typosquatting). Or a maintainer deliberately introduces malicious code (event-stream incident).

This class of attack is harder to detect with CVE-based scanning because there is no CVE — it is a new malicious version of a previously clean package, not a vulnerability in existing code.

Defenses: - Pin exact versions in package-lock.json (do not use npm install --no-save) - Use npm ci (not npm install) in CI to enforce the lockfile - Monitor for unexpected new versions of your direct dependencies

Dependency confusion attacks

An attacker publishes a public npm package with the same name as your internal private package. npm install may prefer the public version. This is a particularly dangerous attack against organizations using private registries.

bash
# Defense: scope your private packages
# Instead of:  require(''my-internal-utils'')
# Use:         require(''@yourcompany/my-internal-utils'')

# In .npmrc, force scoped packages to use your private registry:
@yourcompany:registry=https://your-private-registry.com

npm audit: Strengths and Limitations

npm audit is built into npm and is the first tool most developers encounter.

bash
# Basic audit
npm audit

# JSON output for scripting
npm audit --json

# Fix automatically (conservative)
npm audit fix

# Fix including breaking changes (review before using)
npm audit fix --force

Sample output:

# npm audit report

lodash  <4.17.21
Severity: high
Prototype Pollution - https://npmjs.com/advisories/1523
fix available via `npm audit fix`
node_modules/lodash

2 vulnerabilities (1 moderate, 1 high)

To address all issues, run:
  npm audit fix

What npm audit does well: - Checks your dependency tree against the npm Advisory Database - Available everywhere npm is installed, no additional tools needed - --json output is scriptable - npm audit fix handles straightforward updates automatically

What npm audit misses: - Vulnerabilities not yet in the npm Advisory Database - License compliance issues - Security scoring beyond severity classification - Code pattern analysis (only checks known CVE databases) - Container or OS-level vulnerabilities

In CI, use npm audit as a gate:

bash
# In CI — fail on high/critical
npm audit --audit-level=high || exit 1

# With JSON output for custom processing
AUDIT_OUTPUT=$(npm audit --json 2>/dev/null)
CRITICAL=$(echo "$AUDIT_OUTPUT" | jq ''.metadata.vulnerabilities.critical'')
HIGH=$(echo "$AUDIT_OUTPUT" | jq ''.metadata.vulnerabilities.high'')

if [ "$CRITICAL" -gt 0 ] || [ "$HIGH" -gt 0 ]; then
  echo "FAIL: Found $CRITICAL critical and $HIGH high vulnerabilities"
  echo "$AUDIT_OUTPUT" | jq ''.vulnerabilities | to_entries[] | select(.value.severity == "critical" or .value.severity == "high") | .value.name + ": " + .value.severity''
  exit 1
fi

Snyk: Deeper Analysis

Snyk maintains its own vulnerability database (larger than npm''s) and provides several advantages over npm audit:

  • More vulnerabilities in its database, particularly for less-mainstream packages
  • License compliance checking (useful for enterprise legal requirements)
  • Actionable remediation advice, including upgrade path details
  • Container and IaC scanning in the same tool
  • A free tier for open source projects
bash
# Install Snyk CLI
npm install -g snyk

# Authenticate
snyk auth

# Test for vulnerabilities
snyk test

# Test and fail on high severity
snyk test --severity-threshold=high

# Test a Docker image
snyk container test my-image:latest --severity-threshold=high

# Monitor (continuous tracking in Snyk dashboard)
snyk monitor

Snyk in GitHub Actions:

yaml
      - name: Snyk security scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          command: test
          args: >
            --severity-threshold=high
            --all-projects
            --json-file-output=snyk-results.json

      - name: Upload Snyk results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: snyk-results
          path: snyk-results.json

Understanding CVSS Scores

The Common Vulnerability Scoring System (CVSS) is a standardized 0-10 scale for vulnerability severity. Understanding it helps you triage findings intelligently rather than treating every "high" as equally urgent.

CVSS Base Score ranges:
  0.0       None
  0.1–3.9   Low
  4.0–6.9   Medium
  7.0–8.9   High
  9.0–10.0  Critical

The CVSS vector string explains the score:

CVE-2021-23337 (lodash prototype pollution)
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

AV:N  → Attack Vector: Network (exploitable remotely)
AC:L  → Attack Complexity: Low (no special conditions required)
PR:H  → Privileges Required: High (attacker needs admin access)
UI:N  → User Interaction: None
S:U   → Scope: Unchanged
C:H   → Confidentiality Impact: High
I:H   → Integrity Impact: High
A:H   → Availability Impact: High

Base Score: 7.2 (High)

The key insight: CVSS scores measure the vulnerability''s theoretical maximum severity. Actual risk depends on your specific context:

CVSS 9.8 vulnerabilityActual risk
Used in a production API endpointCritical — fix immediately
Only in a dev dependencyLow — not deployed to production
Requires attacker to have server accessLow — not remotely exploitable
Exploitable only via a specific code path you don''t useLow — not reachable

Always read the vulnerability description, not just the score. A CVSS 9.8 in jest (your test runner) is not a production security issue.


Lockfile Security

The lockfile (package-lock.json or yarn.lock) records the exact version of every package in your dependency tree, including all transitives. Committing the lockfile is a security requirement, not just a convenience.

bash
# Always use npm ci in CI (enforces lockfile, fails if it''s out of sync)
npm ci

# Never use npm install in CI — it can update the lockfile and pull new versions
# npm install   ← don''t use in CI

# Verify lockfile integrity manually
npm ci --ignore-scripts  # also prevents malicious postinstall scripts

Lockfile integrity checks:

bash
# Check for tampering: lockfile should match package.json exactly
npm install --dry-run 2>&1 | grep "added\\|removed" | wc -l
# If this outputs > 0, the lockfile is out of sync with package.json

# Verify the lockfile hash (npm 7+)
npm audit signatures
# This verifies that the packages in your lockfile have not been tampered with

The npm audit signatures command (npm 7.25+) checks that the cryptographic signatures of your installed packages match the registry signatures. This is a direct defense against supply chain attacks where a published package is tampered with after the signature was created.


Automating Updates with Dependabot and Renovate

Manual dependency updates are unreliable — engineers defer them, forget them, or batch them into quarterly "dependency update sprints" that create large diffs and regression risk.

Automated update PRs (Dependabot or Renovate) create one PR per dependency update, keeping updates small and reviewable.

Dependabot (GitHub native)

yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "09:00"
    open-pull-requests-limit: 10
    groups:
      # Group minor/patch updates for non-security packages
      minor-and-patch:
        update-types:
          - "minor"
          - "patch"
    ignore:
      # Don''t auto-update major versions without review
      - dependency-name: "react"
        update-types: ["version-update:semver-major"]
    labels:
      - "dependencies"
      - "automated"

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Renovate (more configurable)

json
// renovate.json
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended"],
  "schedule": ["every weekend"],
  "packageRules": [
    {
      "matchUpdateTypes": ["patch"],
      "automerge": true,
      "automergeType": "pr"
    },
    {
      "matchUpdateTypes": ["minor"],
      "groupName": "minor updates",
      "automerge": false
    },
    {
      "matchUpdateTypes": ["major"],
      "labels": ["major-update", "needs-review"],
      "automerge": false
    },
    {
      "matchCategories": ["security"],
      "schedule": ["at any time"],
      "automerge": true,
      "labels": ["security-fix"]
    }
  ],
  "vulnerabilityAlerts": {
    "enabled": true,
    "schedule": ["at any time"]
  }
}

Renovate''s vulnerabilityAlerts creates PRs for known vulnerabilities immediately, regardless of the regular update schedule. For security updates, you want daily (or immediate) updates — not weekly.


ZeriFlow''s Approach to Dependency Security

ZeriFlow''s Advanced Scan (Pro plan) analyzes your GitHub repository or ZIP upload for security issues including:

  • Hardcoded secrets in source files
  • Known-vulnerable dependency versions based on package.json and package-lock.json parsing
  • Insecure code patterns (SQL injection vectors, prototype pollution, unsafe deserialization)
  • Exposed sensitive file patterns

Unlike npm audit, ZeriFlow also scans your deployed application from the outside — checking whether your application reveals version information that would help attackers identify known vulnerabilities:

X-Powered-By: Express 4.17.1   ← reveals framework version to attackers
Server: nginx/1.14.0            ← reveals web server version

These headers are security-relevant because they confirm to an attacker that specific known-vulnerable versions are in use. ZeriFlow flags their presence and recommends removing them.

Running ZeriFlow on a deployed URL takes 60 seconds and covers the runtime surface — use it in combination with npm audit and Snyk, which cover the source-level surface.


Triaging False Positives

Not every vulnerability report represents actual risk. A structured triage process prevents both under-reaction (ignoring real issues) and over-reaction (burning engineering time on non-issues).

For each finding, answer four questions:

1. Is this package reachable in production?
   → Dev-only dependencies (devDependencies) are not shipped to production.
   → Test frameworks, build tools, linters: typically low actual risk.

2. Is the vulnerable code path reachable?
   → Read the vulnerability description. Does your code call the affected function?
   → Does your application handle user-controlled input in the way the exploit requires?

3. Is the vulnerability exploitable in your deployment context?
   → Many vulnerabilities require local access or specific network conditions.
   → Is the attack vector realistic for your threat model?

4. What is the business impact if exploited?
   → Confidentiality: does it expose user data or credentials?
   → Integrity: can it modify data without authorization?
   → Availability: can it crash your service?

Document your triage decisions in a vulnerability registry:

markdown
| CVE | Package | CVSS | Triage result | Reason | Reviewed by | Review date |
|-----|---------|------|---------------|--------|-------------|-------------|
| CVE-2021-23337 | lodash@4.17.4 | 7.2 | Fix | Reachable via user input | alice | 2026-05-02 |
| CVE-2023-26102 | rangy@1.3.0 | 6.5 | Accept | Dev-only dependency, not deployed | bob | 2026-05-02 |

Accepted risks should have a review date. Treat an accepted risk that has not been reviewed for 90 days as a new finding.


Conclusion

Dependency scanning is a continuous practice, not a one-time audit. The npm ecosystem releases packages and security advisories every day. Your dependency tree is not static. A clean audit today will have findings next month.

The practical baseline for 2026: 1. Run npm audit in CI and fail builds on high/critical 2. Add Dependabot for automated security update PRs 3. Use npm ci (not npm install) everywhere in CI 4. Run Snyk for deeper analysis and transitive dependency coverage 5. Scan your deployed application with ZeriFlow to catch what source-level tools miss

Run an Advanced Scan on ZeriFlow to check your repository for hardcoded secrets, CVE patterns, and insecure code — Pro plan includes GitHub repo and ZIP upload scanning.

Ready to check your site?

Run a free security scan in 30 seconds.

Related articles

Keep reading