Skip to main content
Blog

Server

Vulnerable dependencies: composer, npm and supply chain security

Even clean code becomes vulnerable through an outdated library. composer audit, npm audit, lock files, Dependabot, abandoned packages and SBOM — a practical guide.

· 10 min read

Photo: AgainErick, CC BY-SA 4.0 · Wikimedia Commons

In a modern web application, the code you wrote yourself is a small part of the whole. A Laravel project pulls hundreds of packages into vendor, and a simple React frontend pulls even more into node_modules. Each of them was written by other people, and any of them can turn out to have a vulnerability.

So security does not end with checking your own code. In this post we show how to find vulnerabilities in your dependencies, how to update them and how to automate the process.

What the problem is

There are two kinds of risk:

  1. Known vulnerabilities. A vulnerability is found in a library, gets a CVE, and a fixed version is released. If you stay on the old version, the vulnerability is open and its details are public. OWASP Top 10 (2021) has a dedicated category for this: A06 — Vulnerable and Outdated Components.
  2. Supply chain attacks. The package itself is malicious: its name imitates a popular package (typosquatting), the author's account is taken over and malicious code is added to a new version, or an install script runs code on the server or the developer's machine.

The first risk is routine and easy to manage. The second is rarer but harder to detect.

Code you didn't write is still your code. For the expertise, vendor and node_modules are part of the application.

Why it matters for the expertise

Expertise scanners identify the library versions an application uses in several ways:

  • JavaScript libraries on the frontend (for example, old versions of jQuery or Bootstrap) are identified by file name and the comments inside.
  • An exposed composer.lock, package.json or package-lock.json hands over every package and version in a single list.
  • HTTP headers and error pages can reveal the framework version.

Once a version is identified, the matching known vulnerabilities go into the report as findings. So before submission you need to check your dependencies and make sure lock files are not exposed.

Check:

for f in composer.json composer.lock package.json package-lock.json yarn.lock .env; do
  printf '%s  ' "$f"; curl -s -o /dev/null -w '%{http_code}\n' "https://site.uz/$f"
done

Every response should be 404 or 403. In Laravel, if the web root is the public/ folder, these files naturally stay outside it. If the root is misconfigured, they will be exposed.

How to check it yourself

PHP: composer audit

Since Composer 2.4 there is a built-in audit command. It compares the packages in composer.lock against a database of known vulnerabilities:

composer audit
composer audit --no-dev          # production packages only
composer audit --format=json     # for CI

List outdated packages:

composer outdated --direct

--direct shows only the packages you declared in composer.json yourself — updating them often updates the nested dependencies too.

JavaScript: npm audit

npm audit
npm audit --omit=dev             # production dependencies only
npm audit --audit-level=high     # high and critical only
npm outdated

To find out where a vulnerable package comes from:

npm ls <package-name>

npm audit fix updates to compatible versions. npm audit fix --force may also bump major versions, which can break the application — only use it together with tests.

It is also worth verifying package signatures from the npm registry:

npm audit signatures

Universal: OSV-Scanner

For projects that mix several ecosystems, OSV-Scanner checks all lock files at once against the open OSV database:

osv-scanner scan source -r .

Flutter and Dart

The same principle applies to mobile apps:

flutter pub outdated

Third-party SDKs in a mobile app (analytics, ads, payments) are dependencies too and may be checked during the expertise.

Lock files: reproducible installs

composer.lock and package-lock.json record exactly which versions get installed. Without them, each install may pull in different versions — including new, not-yet-vetted and potentially malicious ones.

Rules:

  • Lock files are committed to the repository.
  • In production and CI, install from the lock file without updating it:
composer install --no-dev --prefer-dist --optimize-autoloader
npm ci --omit=dev

npm install may modify the lock file, while npm ci follows it strictly and fails if it does not match package.json.

  • Review lock file changes in code review. Hundreds of changed lock lines for a small change are a reason to ask questions.

Automation: Dependabot and Renovate

A manual check is useful once, but vulnerabilities are published every day. Connect a tool that proposes updates automatically.

For Dependabot on GitHub, .github/dependabot.yml:

version: 2
updates:
  - package-ecosystem: "composer"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 5
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      minor-and-patch:
        update-types: ["minor", "patch"]

Dependabot opens pull requests with updates every week, and security alerts arrive separately. Renovate is a similar tool that also works on GitLab and other platforms and allows fine-grained grouping and auto-merge rules.

The key is not to let PRs pile up. Build a habit of reviewing and merging them once a week; if the tests are green, don't delay patch updates.

Add a check to CI as well, so code with a vulnerable package never reaches production:

# GitHub Actions steps
- run: composer install --no-interaction --prefer-dist
- run: composer audit --no-dev
- run: npm ci
- run: npm audit --omit=dev --audit-level=high

Abandoned packages

No vulnerability has been published, but the author has stopped maintaining the package — that is a risk too. When the next vulnerability is found, no fix will come.

Composer warns about abandoned packages during install and update. In Composer 2.7 and later, the audit checks this as well, and you can set the policy in composer.json:

{
  "config": {
    "audit": {
      "abandoned": "fail"
    }
  }
}

In npm, deprecated packages show up during installation as npm warn deprecated — don't ignore these warnings. For every abandoned package, find a replacement or remove it.

Protecting the supply chain

  • Check the package name. Before adding a new package, look at its official name, download count, repository and recent activity. A one-letter difference is a sign of typosquatting.
  • Limit install scripts. npm packages can run scripts such as postinstall. If you don't need them in CI, use npm ci --ignore-scripts (but note that some packages don't work without their scripts).
  • Fewer dependencies, less risk. Don't add a package for a one-line task.
  • Add SRI to scripts loaded from a CDN. Then if the file on the CDN is tampered with, the browser won't execute it:
<script src="https://cdn.example.com/lib.min.js"
        integrity="sha384-..."
        crossorigin="anonymous"></script>
  • Don't build on the production server. Build the frontend in CI and deploy the finished artifact — the server needs neither node_modules nor build tools.

SBOM: know what you are made of

An SBOM (Software Bill of Materials) is a list of every component in the application and its version, in a standard format (CycloneDX or SPDX). When a new vulnerability is published, an SBOM lets you answer "do we have this package, and which version?" in seconds.

# npm (npm 10+)
npm sbom --sbom-format cyclonedx > sbom-npm.json

# Composer (via the CycloneDX plugin)
composer require --dev cyclonedx/cyclonedx-php-composer
composer CycloneDX:make-sbom --output-file=sbom-php.json

Store the SBOM with every release. It helps both in the expertise and with customers: you can show exactly what the application consists of.

Short checklist

  1. composer audit --no-dev and npm audit --omit=dev show no serious vulnerabilities.
  2. composer.lock and package-lock.json are in the repository; production uses composer install and npm ci.
  3. Lock files, package.json and .env cannot be opened from a browser.
  4. The frontend has no old jQuery, Bootstrap or similar libraries.
  5. Dependabot or Renovate is connected and PRs are reviewed regularly.
  6. CI stops the build on a vulnerable dependency.
  7. Abandoned and deprecated packages are replaced.
  8. CDN scripts have SRI; nothing is built on the production server.
  9. An SBOM is generated for every release.

Summary

Managing dependencies is not a one-off job but a process. Running composer audit and npm audit once and closing what they find is a good start for the expertise. But in the long run, lock files, automatic updates and CI checks keep the application in a safe state every day. We covered keys and passwords stored in code in our post on managing secrets and keys.

Want to know what state your project's dependencies and server configuration are in? Request a security scan — the Claude AI agent and our specialist will review your codebase and configuration, and we will help close the holes we find and prepare for the expertise.