How to block vulnerable or malicious PHP packages before composer install

You built a client’s site months ago. It works, it has been live for a while, and you have not touched its dependencies since launch. Today there is a small change to ship — a copy tweak, a new template — so you commit and push. Somewhere in your pipeline, composer install runs. It does not add anything new; it simply reinstalls the exact versions already written down in the project, the same ones that have been there for months. Routine. The deploy goes green. You move on.

That boring, green step is where the quiet problem hides. A PHP project records its dependencies in a file called composer.lock — a frozen list of the precise version of every package it uses, both the ones you chose and the hundred-odd they each depend on. It was clean the day it was written. But “clean” has a shelf life. A vulnerability can be discovered in one of those locked versions weeks later, or a maintainer’s account can be hijacked and a poisoned release slipped out under a version you already trust. From that moment on, every composer install faithfully reinstalls the now-dangerous version — and Composer runs that package’s own setup code as part of installing it, before anything has had a chance to look.

This is not hypothetical. In May 2026, attackers using a stolen maintainer’s credentials pushed malicious releases across roughly 700 versions of the widely used Laravel-Lang package in minutes — code that executed through Composer’s post-install hooks the instant it was installed. Any project that happened to run composer install in that window was compromised before a human could even read the news.

Composer has since added genuine defences here: recent versions block known-bad advisories and malware by default when you add or update packages, and composer audit will report trouble after the fact. But one specific door stays open — the reinstall of an already-locked version that only became dangerous after it was locked. composer-cve-gate is built to close that particular door.

The problem, in plain terms

Most people picture the risky moment as the one where you add a new package. For PHP it is just as often the opposite: the install you run on every single deploy, from a lockfile you have not changed in months. Composer code can execute during installation itself — on the autoload bootstrap, or when a composer-plugin package loads — so a bad version runs as it installs, well before composer audit (which only looks afterwards) gets a turn.

Native Composer blocking helps when you change dependencies, but it does not re-judge a version that was already in your lockfile and clean at the time. That is the install-from-lock gap: clean-at-commit is not the same as clean-at-deploy, and the gap between them can be weeks long.

Who runs into this

Agencies and freelancers redeploying sites they built long ago; any team whose CI runs composer install on every push; the maintainer who pinned a lockfile a year ago and assumes “I haven’t changed anything” means “nothing has changed.” The packages did not move — but the world’s knowledge about them did.

What composer-cve-gate does — and how it works

composer-cve-gate is a Composer plugin. It adds three commands — safe-install, safe-upgrade and safe-scan — and, most importantly, it can gate the plain composer install that runs from your lockfile.

How composer-cve-gate blocks a bad package at installA composer install from the lockfile is intercepted by the gate, which checks every locked package across five signals before any post-install script runs, blocking a flagged one.composer installruns scripts on installcomposer-cve-gate5 signals + freshness,from the lockfileBlockedbefore scripts run
The lockfile is checked before composer install runs — a flagged package never reaches your project.

It closes the install-from-lock gap

This is the durable reason the tool exists. With the gate switched to block mode, a plain composer install reads the lockfile, checks every locked package, and aborts before any download or post-install script runs if one is flagged — catching exactly the case native Composer policy passes over: a version that was fine when you committed it and dangerous by the time you deploy.

Five independent signals, queried directly

Every package — and its transitive tree — is checked against the OSV database, the GitHub Advisory Database and NIST NVD for known vulnerabilities, plus the OpenSSF malicious-packages registry for confirmed malware. composer-cve-gate queries these feeds itself rather than relying on a single source, so a gap in one is covered by another.

A freshness hold for brand-new releases

Any package published less than three days ago is held back by default — the window in which a compromised release is usually live on Packagist but not yet in any advisory feed. It is a deliberately temporary feature: when Composer ships a native release-age policy, this part retires. Need a specific fresh version you trust? --min-age 0 lets it through.

“Am I already infected?” — safe-scan

After an incident hits the news, safe-scan reads your lockfile, re-checks every installed package, and walks vendor/ on disk for indicators of compromise — attacker C2 domains, exfiltration URLs, injected file paths. It never executes or downloads anything; it just answers the question you actually have at 2 a.m.: is this project already carrying something bad?

Install & use it

Install

composer require sharkyger/composer-cve-gate --dev

The plugin self-registers — the three commands appear in composer list immediately, with no config file to write. On a DDEV project (TYPO3, Drupal, Laravel, Symfony…) install the add-on instead, so the scan runs inside the web container against the PHP version your app actually uses:

ddev add-on get sharkyger/composer-cve-gate

Use it

composer safe-install monolog/monolog     # scan a new package + its tree, then install only if clean
composer safe-upgrade                      # scan every direct dependency, then update
composer safe-scan                         # audit what is already installed (+ on-disk infection check)

To make plain composer install fail the build on a finding (instead of warning), switch the gate to block mode in your root composer.json:

{ "extra": { "composer-cve-gate": { "install-gate": "block" } } }

Full configuration, exit codes, the DDEV workflow and a reproducible Docker proof are on GitHub: github.com/sharkyger/composer-cve-gate (MIT).

FAQ

How is composer-cve-gate different from composer audit?

composer audit runs after the install, reporting problems once a package’s code is already on disk and may have run. composer-cve-gate checks at composer install time, from the lockfile, and blocks a flagged package before Composer downloads it or runs its post-install scripts.

Doesn’t Composer already block bad packages now?

Recent Composer versions do block known advisories and malware by default when you add or update packages — a real improvement, and you should keep it on. The gap composer-cve-gate fills is the reinstall: when a vulnerability is published after your composer.lock was committed, a later composer install reloads that locked version with no block. composer-cve-gate gates that path, and adds a freshness hold and an on-disk infection scan on top.

Does it work with TYPO3, Drupal or Laravel via DDEV?

Yes. A DDEV add-on runs the scanner inside the web container against the PHP version your app actually uses, and exposes ddev safe-install, safe-upgrade and safe-scan — so the PHP version on your host is irrelevant.


composer-cve-gate is one of the 5bats supply-chain gates — the same pre-install thinking applied to Python, Homebrew, and the packages an AI assistant installs.

See the source, configuration and DDEV guide on GitHub →