A malicious package name that treats your filesystem like it owns the whole tree, not just vendor/
Composer trusts package names too much. A crafted name in a composer.json requirement (or in a package resolved transitively from an untrusted repository) can escape the vendor/ directory during install/update, writing arbitrary files anywhere the Composer process has permission. In practice that means dropping a .bashrc, ~/.ssh/authorized_keys, or a cron entry — turning a dependency pull into arbitrary code execution as the user running composer. Affected: Composer 1.0 through 2.2.28 and 2.3.0 through 2.10.1. Fixed in 2.2.29 and 2.10.2 (GHSA-499r-g7pc-vmp9).
The vendor's HIGH / 7.0 rating is defensible and we're keeping it. The CVSS vector (AV:L/AC:H/PR:N/UI:R) accurately reflects that this is not a wormable network flaw — it needs a malicious package and a user action to install it. But CVSS underweights *where Composer actually runs*: CI runners, container build stages, and dev workstations with SSH keys and cloud credentials. Arbitrary file write on any of those is not a workstation nuisance — it's a foothold in the supply chain.
5 steps from start to impact.
Attacker publishes or compromises a package
composer publish flow, or account takeover via credential stuffing / phishing against maintainers.- Ability to publish to a repository the victim consumes
- Victim uses a repository other than Packagist-with-strict-name-validation, OR uses a repo added via
repositories:in composer.json
- Packagist itself validates names on submission for new packages
- Most enterprises pin dependencies and don't chase floating versions
Victim runs composer install/update/require
composer install or composer update and the malicious package name gets resolved. This is the UI:R in the CVSS vector — but on a CI runner, 'user interaction' means 'the pipeline ran', which happens hundreds of times a day. Tooling: any invocation of composer binary, php composer.phar, or the composer step in a Dockerfile / GitHub Actions workflow.- The malicious package is reachable via the victim's configured repositories
- Composer version is 1.x–2.2.28 or 2.3.0–2.10.1
- Lockfile pinning (
composer.lockcommitted) prevents new packages from being pulled unnoticed --no-scripts/--no-pluginsdoesn't help here (this isn't a script-hook flaw)
Path traversal writes file outside vendor/
../ or absolute-path components escape vendor/ and write to attacker-chosen locations — $HOME/.bashrc, $HOME/.ssh/authorized_keys, /etc/cron.d/* (if root), or the CI workspace's post-build hook directories.- Composer process has write permission to the target file/directory
- Target path exists or its parent directory is writable
- Composer typically runs as a non-root user on CI, limiting
/etc/cron.dwrites - Immutable/read-only container filesystems block most useful targets
watch on ~/.ssh, ~/.bashrc, /etc/cron.*) catches the write. EDR with FIM rules on developer/CI hosts will flag it.Code execution on next shell or scheduled trigger
.bashrc), next SSH connection (authorized_keys allowing attacker in), or next cron tick. On a CI runner this typically becomes whoami = the CI user, with access to build secrets, cloud tokens, artifact-signing keys, and network egress. Tooling: the attacker's implant is usually a small reverse shell or a credential harvester tailored to the CI environment.- A shell login, SSH connection, or cron trigger occurs after the write
- Written file survives long enough to fire (ephemeral runners narrow this window)
- Ephemeral CI runners (fresh container per job) destroy the write before it fires — unless the payload targets in-job secret exfil directly
- MFA-gated SSH and hardware key requirements neutralize
authorized_keysin most enterprise setups
bash, sshd, or cron; SIEM correlation on 'first-time process' from a CI runner.Supply-chain pivot from build server
- Composer ran on a build/CI system that produces signed or distributed artifacts
- Build secrets are reachable from the composer process
- Hardware-backed signing (HSM, GitHub artifact attestations, Sigstore) prevents key theft
- Just-in-time secret injection with short TTLs limits blast radius
The supporting signals.
| In-the-wild exploitation | No confirmed exploitation reported as of 2026-07-15. No mentions in threat-intel feeds, no incident writeups. |
|---|---|
| Proof-of-concept | No public PoC in Exploit-DB, GitHub, or Metasploit at time of assessment. Advisory text (shell startup files, authorized_keys, cron) provides enough detail for a competent researcher to reproduce. |
| EPSS | 0.00132 (~0.13%) — bottom of the exploitation-likelihood distribution, consistent with 'no public PoC, requires malicious package'. |
| CISA KEV | Not listed. Highly unlikely to be added — KEV favors network-reachable, unauthenticated bugs. |
| CVSS vector | AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H — local attack vector, high complexity (needs the malicious-name payload to be resolved), no privileges, user interaction required. Full CIA impact once landed. |
| Affected versions | Composer 1.0 – 2.2.28 and 2.3.0 – 2.10.1 |
| Fixed versions | 2.2.29 (LTS branch) and 2.10.2. Distro backports: check Debian/Ubuntu composer package, Alpine composer, RHEL/Fedora php-composer2 — most distros ship 2.x and will backport. |
| Sibling CVEs in same release | CVE-2026-59946 (bin-path traversal, MEDIUM) and CVE-2026-59947 (URL token leakage in verbose output). Patch all three at once via the 2.10.2 upgrade. |
| Exposure population | Composer is on effectively every PHP dev workstation and every CI runner that builds PHP — millions of hosts. Shodan/Censys don't scan for it (not a network service), but SBOM prevalence data (Sonatype, Snyk) puts it in the top-tier of package managers. |
| Disclosure & attribution | Published 2026-07-08 via GitHub Security Advisory GHSA-499r-g7pc-vmp9. Reporter credited in the Composer changelog for 2.10.2. |
noisgate verdict.
The single most decisive factor is the role multiplier: Composer runs on CI/CD build servers, and an arbitrary file-write primitive on a build runner is a supply-chain pivot — that keeps the floor at HIGH regardless of the local-only, UI-required CVSS vector. The friction (needs a malicious package pulled from an untrusted repo, needs someone to run composer) prevents an upgrade to CRITICAL but is not sufficient to break the HIGH floor set by the deployment role.
Why this verdict
- Vendor baseline is fair. 7.0 accurately captures a local, UI-required file-write-to-RCE bug. Nothing about the underlying tech screams underrated.
- Friction adjustment: −0.5. Needs a malicious package on a repository the victim consumes, needs a
composer install/update, and needs write access to a meaningful target. Enterprise Packagist mirrors and committedcomposer.lockfiles kill most opportunistic attempts. - Role multiplier: +1.0 — floor is HIGH. Composer's canonical deployment includes CI/CD build servers (Jenkins, GitLab Runner, GitHub Actions self-hosted, Bitbucket Pipelines, Bamboo). File write on any of those = access to build secrets, artifact-signing keys, and downstream distribution. That is a supply-chain pivot, and it easily represents ≥10% of the installed base — Composer is *native* to build servers, not a peripheral. The HIGH floor is firm.
- Role multiplier: +0.0 on dev workstations. Compromised laptop = one developer's git creds, cloud tokens, SSH keys. Bad, but bounded. Doesn't push the score higher on its own.
- No exploitation multiplier. EPSS 0.13%, no KEV, no PoC. This is not a 'drop everything' bug — it is a 'patch it in the normal HIGH lane' bug.
Why not higher?
No active exploitation, no public PoC, and the delivery mechanism requires an attacker to first get a malicious package into your dependency graph — a non-trivial prerequisite for any organization that uses Private Packagist, a proxy repo (Nexus/Artifactory), or committed lockfiles. CRITICAL is reserved for chains that are network-reachable or have KEV evidence; this is neither.
Why not lower?
MEDIUM would ignore the deployment-role reality: Composer runs on CI, and file-write-to-RCE on CI is a supply-chain vector. The role multiplier explicitly floors this at HIGH — friction alone cannot pull it below. Even if you have strong dev-workstation controls, a single unpinned dependency on one build agent is enough for full impact.
What to do — in priority order.
- Upgrade Composer to 2.10.2 (or 2.2.29 on the LTS branch) fleet-wide — This is the actual fix. Push it via your config-management tool (Ansible/Chef/Puppet), your base container images, and your CI runner provisioning. Deploy within 30 days per the noisgate mitigation SLA for HIGH — but on CI/build hosts, treat as the top of the queue and aim for 7 days.
- Route all Composer traffic through a private proxy (Private Packagist, Nexus, Artifactory, Toran Proxy) — Prevents unreviewed third-party repositories from reaching your build hosts. Even after patching, this is the single highest-leverage supply-chain control for PHP shops. Deploy within 30 days.
- Commit and enforce
composer.lockin every PHP repository, and requirecomposer install(notupdate) in CI — Locks the exact package name+version set. A malicious name can't be resolved unless it's already in the lockfile — which turns the attack into an obvious PR-review event. Roll out via repo-template policy and CI linter. - Enable file-integrity monitoring on CI runners and dev workstations for
~/.ssh/,~/.bashrc,~/.zshrc,~/.profile,/etc/cron.*,/etc/profile.d/— Detects the exploit primitive directly, regardless of which CVE delivered it. Feed alerts to SIEM with high priority when the writing process isphporcomposer. - Move CI to ephemeral runners with read-only base images — Even if exploitation succeeds, the container is destroyed at job end and the payload can't persist. Combine with just-in-time secret injection (OIDC-federated cloud creds, short-TTL vault leases) to minimize what any single compromised job can steal.
composer install --no-scripts/--no-plugins— this bug is in the *name-resolution* path, not the post-install script hook. These flags don't touch it.- Network egress restrictions on CI — Composer legitimately needs to reach package registries; you can't blanket-block it without breaking builds.
- Perimeter WAFs, IPS, TLS inspection — this is a local file-write vulnerability triggered by a legitimate dependency pull. Nothing on the wire looks anomalous.
- Running Composer as non-root — helps limit
/etc/cron.dwrites but does NOT stop the attack:~/.bashrcand~/.ssh/authorized_keysare writable by the running user by definition, and that's the whole payload.
Crowdsourced verification payload.
Run on each host that has Composer installed — dev workstations, CI runners, build agents, container base images. Invoke as bash check-composer-59948.sh with no arguments; needs only read access to the composer binary in PATH. Exits 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN.
#!/usr/bin/env bash
# check-composer-59948.sh
# Detects Composer versions vulnerable to CVE-2026-59948 (arbitrary file write via package name)
# Fixed in 2.2.29 (LTS) and 2.10.2
set -u
COMPOSER_BIN="$(command -v composer || true)"
if [ -z "$COMPOSER_BIN" ]; then
# Also try common alt locations
for c in /usr/local/bin/composer /usr/bin/composer ~/.composer/vendor/bin/composer /opt/composer/composer.phar; do
[ -x "$c" ] && COMPOSER_BIN="$c" && break
done
fi
if [ -z "$COMPOSER_BIN" ]; then
echo "UNKNOWN: composer binary not found in PATH or common locations"
exit 2
fi
VER_LINE="$($COMPOSER_BIN --version --no-ansi 2>/dev/null | head -n1)"
# Example: "Composer version 2.7.1 2024-02-09 15:26:28"
VER="$(echo "$VER_LINE" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)"
if [ -z "$VER" ]; then
echo "UNKNOWN: could not parse version from: $VER_LINE"
exit 2
fi
# Split version
IFS='.' read -r MAJ MIN PATCH <<< "$VER"
vuln=1 # assume vulnerable until proven otherwise
if [ "$MAJ" -eq 2 ] && [ "$MIN" -ge 3 ]; then
# 2.3.0 - 2.10.1 vulnerable; 2.10.2+ patched
if [ "$MIN" -gt 10 ] || { [ "$MIN" -eq 10 ] && [ "$PATCH" -ge 2 ]; }; then
vuln=0
fi
elif [ "$MAJ" -eq 2 ] && [ "$MIN" -le 2 ]; then
# 2.0.0 - 2.2.28 vulnerable; 2.2.29+ patched on LTS branch
if [ "$MIN" -eq 2 ] && [ "$PATCH" -ge 29 ]; then
vuln=0
fi
elif [ "$MAJ" -ge 3 ]; then
# Future majors assumed patched
vuln=0
fi
if [ "$vuln" -eq 0 ]; then
echo "PATCHED: composer $VER at $COMPOSER_BIN"
exit 0
else
echo "VULNERABLE: composer $VER at $COMPOSER_BIN — upgrade to 2.10.2 (or 2.2.29 on LTS)"
exit 1
fi
If you remember one thing.
composer.lock files, and turn on FIM for ~/.ssh/, shell rc files, and /etc/cron.* on CI runners. Per the noisgate remediation SLA for HIGH, upgrade Composer to 2.10.2 (or 2.2.29 on the LTS branch) across dev laptops, CI runners, and container base images within 180 days — but prioritize build servers to the front of that window (aim for 7 days on CI/build hosts specifically, because that's where the supply-chain blast radius lives). No KEV, no PoC, EPSS is negligible; skip the emergency change-management channel unless your threat model includes targeted supply-chain adversaries.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.