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
composer.lock in every PHP repository, and require composer install (not update) 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.~/.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 is php or composer.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.
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. |
Sources.
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.
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