Lockhound
Guides

How to find vulnerable packages in composer.lock, and which to fix first

Use composer audit to list known vulnerabilities in a PHP project, read what it tells you, trace transitive packages, and decide what to upgrade first.

Oct 30, 2026 · 1 min read

Every Laravel, Symfony or WordPress-plugin project carries dozens to hundreds of Composer packages, most of them pulled in by something else. When an advisory lands for one of them, you need to know three things quickly: is the version you actually run affected, how did it get into the project, and is it worth dropping what you're doing for.

This guide covers the tools Composer gives you for each step, and where they stop.

Start from composer.lock, not composer.json

composer.json says what you allow, for example "guzzlehttp/guzzle": "^7.4". composer.lock records what was actually installed: one exact version of every package, including the ones you never asked for directly.

A version range can't be checked against an advisory with certainty. ^7.4 might mean 7.4.0, which is affected by a bug, or 7.9.2, which isn't. So check the lockfile, and commit it, even for libraries you only run in CI.

Run composer audit

Recent Composer 2 releases ship an audit command that checks installed packages against the security advisories Packagist publishes:

composer audit --locked

--locked reads composer.lock instead of the vendor/ directory, so it works in CI before composer install, and on a fresh clone. To feed another tool, ask for JSON:

composer audit --locked --format=json

The command exits with a non-zero status when it finds advisories, which is what makes it useful as a CI gate. Composer also prints an audit summary after composer require and composer update, so you see new problems as you introduce them.

A typical result looks like this:

Found 1 security vulnerability advisory affecting 1 package:
+-------------------+----------------------------------------------------------------------------------+
| Package           | guzzlehttp/psr7                                                                  |
| Severity          | high                                                                             |
| CVE               | CVE-2022-24775                                                                   |
| Title             | Improper Input Validation in guzzlehttp/psr7                                     |
| Affected versions | <1.8.4|>=2.0.0,<2.1.1                                                            |
+-------------------+----------------------------------------------------------------------------------+

The affected versions line is the one to read carefully. It tells you the lowest version that is safe from this advisory: here 2.1.1 on the 2.x line, or 1.8.4 if you are still on 1.x.

Find out why a package is installed

Most vulnerable packages are transitive: you don't require them, something you require does. Before upgrading, find the path:

composer why guzzlehttp/psr7

This lists the packages that depend on it, with their constraints. If the parent allows the fixed version, one command updates it in place:

composer update guzzlehttp/psr7 --with-all-dependencies

If the parent pins an old range, composer update won't move past it. Then you either upgrade the parent, or ask why it blocks you:

composer why-not guzzlehttp/psr7 2.1.1

For a package you require directly, raise the constraint itself:

composer require "guzzlehttp/psr7:^2.1.1" --with-all-dependencies

Check that the fix isn't vulnerable too

An advisory tells you which version fixes that advisory, not that the version is free of others. Packages with several advisories often need a later release than the first one listed. After upgrading, run composer audit --locked again. If it still reports the package, read the new affected range and go one step further.

Decide what to fix first

On an older project composer audit can easily list twenty advisories. They don't all deserve the same urgency:

  1. Exploited in the wild. If the CVE is in CISA's Known Exploited Vulnerabilities catalog, attackers are using it. Fix these first, whatever the severity score says. Composer doesn't show this; our guide to KEV and EPSS shows how to look it up.
  2. Critical and high in code that ships. Packages in the packages section of composer.lock are installed in production. Packages under packages-dev (PHPUnit, Pest, code-style tools) usually aren't, unless you deploy with composer install without --no-dev.
  3. Everything else, in the next routine dependency update.

Check how you deploy before trusting the dev split: if production runs composer install without --no-dev, dev packages ship too.

Make it continuous

Running the audit once is a snapshot. New advisories are published every day for versions you already have, so a project that was clean last month may not be today. Three ways to keep up:

  • In CI: run composer audit --locked on every pull request so new vulnerabilities can't be merged unnoticed.
  • On a schedule: run it nightly on your main branch, because the advisory data changes even when your code doesn't.
  • With a monitoring service such as Lockhound, which re-checks every repository daily, ranks findings by exploitation and scope, and fails pull requests that add new risk.

Whichever you choose, keep composer.lock committed: every one of these tools depends on it.

Read next