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:
- 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.
- Critical and high in code that ships. Packages in the
packagessection ofcomposer.lockare installed in production. Packages underpackages-dev(PHPUnit, Pest, code-style tools) usually aren't, unless you deploy withcomposer installwithout--no-dev. - 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 --lockedon 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
- Which vulnerable dependencies to fix first, using CISA KEV and EPSSCVSS tells you how bad a vulnerability could be, not whether anyone is exploiting it. Here is how to add CISA's Known Exploited Vulnerabilities catalog and FIRST's EPSS scores to your triage, with commands you can run today.
- Composer and PHP dependency security, across every repositoryLockhound reads composer.lock in every GitHub repository, finds vulnerable, exploited and malicious packages, separates production from require-dev, and checks every pull request.