Pre-install / pre-upgrade CVE gate for Composer. Blocks before post-install scripts run.
Most of the time composer require is fine. Sometimes it isn't — and
when it isn't, the damage is usually done by the time composer audit
flags it, because audit runs after post-install scripts.
composer-cve-gate adds two subcommands that resolve the full
transitive tree, check every package against multiple vulnerability
signals, and block the install before any code touches your machine.
What it checks
- OSV.dev — Google's aggregated vulnerability feed, native Packagist coverage.
- GitHub Advisory Database — composer ecosystem, version-range filtered.
- NIST NVD — keyword + CPE-version match for upstream CVEs.
- Packagist freshness hold — packages published less than 3 days
ago are held. Defends against typosquats and zero-hour publish
attacks (the malicious version is up before any CVE database
knows). Override with
--min-age 0when needed. - OSSF Malicious Packages — direct ingestion of the OpenSSF
ossf/malicious-packagesfeed (aggregates Aikido, StepSecurity, Snyk, and other early reporters). Closes the gap after the 3-day freshness window expires, when security researchers have published advisories but no CVE has been assigned yet.
All five signals run against the resolved transitive tree, not just the package you typed. A vulnerable transitive dependency is treated the same as a vulnerable direct dependency.
Why pre-install matters
Composer's post-install hook system lets a package run arbitrary code
the moment it's downloaded. If the malicious version isn't blocked
before download, post-install can ship credentials, install a
backdoor, or rewrite local files — all before composer audit is
even allowed to look at the lockfile. Pre-install gating is the only
point in the lifecycle where blocking is still useful.
Install
composer require sharkyger/composer-cve-gate --dev
That's it — the plugin self-registers and both subcommands appear in
composer list immediately. No config file, no per-project setup.
Requirements
| Component | Version | Why |
|---|---|---|
| Composer | ^2.0 |
Plugin uses the modern composer-plugin-api v2 hook |
| PHP | ^8.2 |
Modern constructor promotion, readonly, enum |
| Python | ≥ 3.11 |
Scanner uses datetime.UTC (Python 3.11+) |
The bundled scanner (bin/dependency_security_check.py) is invoked as
a subprocess — python3 must be on PATH. The scanner has zero
third-party Python dependencies (only stdlib + the optional certifi
bundle on macOS for SSL trust). If Python is missing at activation,
the plugin fails loud immediately rather than disabling itself
silently.
Usage
Install a new package, scanned first
composer safe-install monolog/monolog
The plugin resolves monolog/monolog plus its full transitive tree,
queries every package against OSV / GHSA / NVD plus the freshness
hold, and only proceeds with the actual install if everything is
clean. Output on a clean scan:
safe-install: scanning monolog/monolog
[standard composer require output follows]
If something is blocked, you'll see a structured report and nothing installs:
safe-install: scanning evil/pkg
BLOCKED: evil/pkg@1.0.0 — status=vulnerable
[CRITICAL] CVE-2026-XXXX — info-stealer in post-install script
safe-install: blocked 1 of 1 package(s). Nothing installed.
Exit code is 1. Your project is untouched — no download, no
vendor/ write, no post-install scripts run.
Install a dev dependency
composer safe-install --dev phpstan/phpstan
--dev is forwarded to composer require, so the package lands in
require-dev as expected.
Upgrade all dependencies
Scans every direct dependency from your composer.json, then
delegates to composer update with no package args — composer
resolves the full graph (including transitive-only updates).
Upgrade one package
composer safe-upgrade vendor/pkg
Scans then runs composer update vendor/pkg.
Install a brand-new release
The 3-day freshness hold blocks installs of packages published less than 72 hours ago — that's the window where a compromised version is most often up on Packagist but not yet in any CVE database. If you know a particular fresh release is fine (e.g. a patch you've been waiting for from a maintainer you trust), pin to that version and disable the hold:
composer safe-install --min-age 0 vendor/just-released:1.2.3
Audit what's already installed
Reads composer.lock to enumerate every installed dependency, runs
the full pre-install scan against each, and additionally walks
vendor/<package>/ looking for indicator-of-compromise strings or
marker files from any known-malicious finding (C2 domains, exfil URLs,
attacker-injected file paths). Output categorises packages as:
=== safe-scan report ===
INFECTED — 1 package(s):
evil/pkg@1.0.0
[url] https://evil.test/exfil → vendor/evil/pkg/src/payload.php
safe-scan — 12 clean, 0 suspicious, 1 infected (of 13 scanned).
| Status | Meaning |
|---|---|
CLEAN |
No findings, no IoC matches. |
SUSPICIOUS |
Vulnerability database hit, but no IoC strings on disk. |
INFECTED |
IoC strings or marker files found inside the installed package. |
Read-only — safe-scan never executes, modifies, or downloads
anything. It's the answer to "am I already infected?" after a
supply-chain incident hits the news.
Reading exit codes
safe-install / safe-upgrade:
| Exit code | Meaning |
|---|---|
0 |
Scan clean, install proceeded |
1 |
At least one package blocked, nothing installed |
2 |
Scanner errored (network, missing Python, etc.) |
safe-scan:
| Exit code | Meaning |
|---|---|
0 |
Clean |
1 |
Infected (IoC matches found on disk) |
2 |
Suspicious (vulnerability findings but no IoCs on disk) |
3 |
Scanner error (lockfile missing, malformed, etc.) |
When you see a BLOCKED line, the next step is to look up the CVE
or advisory ID it cites and decide whether the issue actually
applies to your usage. If it doesn't, you have two paths:
- Pin to a patched version explicitly:
composer safe-install vendor/pkg:^2.1.4 - Disable the freshness hold for a one-off (only if the block came
from
FRESH-HOLD, not from a CVE):composer safe-install --min-age 0 vendor/pkg
Scope
composer-cve-gate does one thing: block known-vulnerable installs
before they happen.
It is not a replacement for composer audit (post-install lockfile
scanning), not a malware scanner, not an SBOM tool, not a CI security
suite. Those are different products. We stay out of their lane on
purpose — sharper identity, fewer ways to dilute the gate.
DDEV
If your project uses DDEV (TYPO3, Drupal, Laravel, Symfony, Magento, …), install the addon instead of the composer plugin directly. The addon runs the scanner inside the web container against the container's PHP version — which is the version your application actually runs — rather than whatever PHP happens to be on your host.
ddev add-on get sharkyger/composer-cve-gate
That registers three custom commands and auto-installs the composer
plugin into your project (if composer.json exists):
ddev safe-install monolog/monolog ddev safe-upgrade ddev safe-scan
Each one runs in the web container and applies the same 5-signal gate the plain-composer commands do. No host shim — your host PHP version is irrelevant.
Remove the addon with ddev add-on remove composer-cve-gate, which
also removes the composer plugin from your project.
Related projects
composer-cve-gate is part of the safe-install family alongside:
claude-code-cve-gate— Claude Code hook (intercepts AI installs)mistral-code-cve-gate— Mistral Code hookhomebrew-safe-upgrade—brew safe-install/brew safe-upgrade
All share the same OSV + GHSA + NVD + freshness-hold pattern.
License
MIT. See LICENSE.
Security
Report vulnerabilities privately to sharky@augatho.com. See SECURITY.md. This repo does not accept public bug reports for security topics.