Blog
Composer Supply Chain Attack Checklist for PHP Projects
Attackers hit Packagist three times this spring with stolen GitHub access. Here's what happened, what Composer changed, and the checklist I use to lock down a PHP project's dependencies.
Caleb Dorris 22 October 2026, 12:00 PM Updated 30 October 2026 11 min

Update, October 29, 2026 (revised October 30)
Composer 2.11 and 2.2.31: security fixes, and the cooldown is here
On October 29, Composer shipped 2.11.0 and 2.2.31, the long-term support release for projects stuck on PHP 5.3 to 7.1. Both fix two published security advisories and a handful of smaller bugs. 2.11 also ships the cooldown policy I mention below, and removes source fallback for good. On October 30, 2.11.1 followed with bug fixes, so that's the version to install.
What got fixed
- API tokens could end up in
composer.lock(GHSA-fm2w-xgf2-f9fh, rated medium, CVSS 5.3). Some private repositories are set up with an inline HTTP header (options.http.header) that carries a token, often in your global Composer config. Composer copied that header intocomposer.lock, which most teams commit. Credentials kept inauth.jsonorCOMPOSER_AUTHwere never written there. - A tampered
installed.jsoncould reach outsidevendor/(GHSA-c968-33g3-wh7p, rated high, CVSS 7.1). A crafted package name invendor/composer/installed.jsoncould makecomposer installdelete a directory outsidevendor/, andcomposer reinstallwrite files there. Composer says publishing a package can't trigger it. An attacker has to be able to write that file. The realistic case is a CI cache ofvendor/that untrusted jobs, like pull requests from forks, can also write to. - Smaller fixes. Extra arguments passed to
@composerscripts weren't escaped. Repository headers could be sent to other hosts whose names merely start with your repository's host. And Composer now rejects the compromised eMudhra intermediate certificates even if your system trusts their roots. That's the same certificate problem behind the WordPress 7.1.4 and Python updates in my October security patches roundup. On 2.2.31, that fix needscomposer/ca-bundle1.4.16 or later.
What to do this week
- Upgrade everywhere Composer runs.
composer self-updategets you 2.11.1. Skip 2.11.0: it can abort everycomposer updatewith aSecurityExceptionwhen a PHP extension reports a name that isn't a valid platform package name, and 2.11.1 fixes that. It also fixes GitLab CI job tokens failing against GitLab's Composer package registry, plus a PHP 8.6 deprecation warning. On the LTS line,composer self-update --2.2gets 2.2.31. It has the same update bug, and the fix is merged but hadn't shipped in a 2.2 release as of the morning of October 30. If Composer came from a distro package, update it there. Then checkcomposer --versionon laptops, CI runners and deploy servers. - Check your lock files for leaked tokens. Look for
headerentries undertransport-optionsincomposer.lock, in the current file and in git history, for example withgit log -p --all -- composer.lock | grep '"header"'. After upgrading,composer update --lockrewrites the lock without them. If a token shows up anywhere it was shared, revoke and rotate it. Deleting it from the latest commit doesn't remove it from history. My API key rotation checklist covers the order. - Don't trust a shared
vendor/cache. If CI restoresvendor/from a cache that fork PRs or other low-trust jobs can write to, scope that cache to trusted jobs or rebuild withcomposer install. - Check for
source-fallback. It was a temporary option in 2.10, and 2.11 removed it. A failed dist download now just fails instead of cloning the repo. If you really need source installs, ask for them withpreferred-installor--prefer-source. - Consider a cooldown. It's off by default. To hold back anything published in the last day, add this to
composer.json:{"config": {"policy": {"cooldown": {"period": "1 day"}}}}. Composer's team suggests a few hours to a day, because security fixes wait out the cooldown too. There's no automatic exemption. For a fix you need now, runcomposer update --no-cooldown vendor/package. Put your own packages underignore. If your project already has apolicyconfig, expect a lock-file-outdated warning after upgrading until you update the lock.
If you're already patching PHP itself for PHP2Shell, do Composer in the same pass. If you'd like a second pair of eyes on a PHP or WordPress project's dependencies, get a free quote.
For years, a Composer supply chain attack was something PHP developers read about happening to npm. That changed in 2026. Between April 30 and May 23, attackers used stolen GitHub access to push malware into real, widely installed Packagist packages, not typosquats.
This week's PHP2Shell vulnerabilities were about the PHP runtime itself. This is the other half of the picture: the code you pull in with composer require. I spend most days in PHP projects with a vendor/ folder full of other people's code, so here's what happened, what Composer and Packagist changed, and the checklist I actually run.
What happened to Packagist in 2026?
Three incidents stand out, and each one used a different way to get code running.
intercom/intercom-php, April 30. The attacker changed the 5.0.2 tag to point at a new commit. The poisoned version turned the API client into a Composer plugin that ran on post-install-cmd and post-update-cmd, downloaded the Bun runtime, and ran an obfuscated credential stealer. Socket says it flagged the code 14 minutes after release, and Packagist pulled it. Intercom traced the root cause to a compromised Python package installed on one machine, which shows how these attacks hop between ecosystems.
laravel-lang, May 22 to 23. A leaked GitHub token let the attacker rewrite historical release tags across four laravel-lang/* packages. Snyk counts about 700 poisoned versions. The payload was a src/helpers.php file registered under autoload.files, so it ran on every PHP request, not just at install. It went after cloud keys, .env files, SSH keys and CI secrets.
Eight dev-branch packages, May 23. These hid a postinstall script in package.json, not composer.json, aimed at projects that run npm build steps next to their PHP code. Every affected version was a dev-* branch.
The pattern is the same in all three: nobody broke Composer. They took over a maintainer's GitHub access and published something that looked legitimate.
What changed after the Composer supply chain attacks
Packagist and the Composer team moved fast.
- Stable versions are now immutable on Packagist.org. If someone re-tags a published version upstream, Packagist rejects the change and emails the maintainers. That's the exact trick the intercom and laravel-lang attacks used.
- Composer 2.10, released May 28, blocks known malware by default. Packagist imports malware flags from Aikido. Flagged versions can't be installed by
updateorrequire, and they failcomposer installeven if they're already in yourcomposer.lock.composer auditfails on them too. You don't need to configure anything. - Source fallback is off. If a dist download fails, Composer 2.10 no longer quietly falls back to cloning the git repo, which could have pulled a tampered tag. Composer 2.11 removed the temporary
source-fallbackoption that turned it back on. - A cooldown policy shipped in 2.11. It holds back versions published in the last N hours or days, since most malicious releases are caught within that window. Composer 2.11.0 came out October 29. The cooldown is off until you set a period. See the update at the top.
That's real progress. It also means a lot of protection depends on whether your servers and CI are actually on a current Composer.
How do I protect my PHP project from malicious packages?
Here's my checklist. None of it is exotic, and most of it takes an afternoon.
- Update Composer everywhere. Run
composer --versionon your laptop, CI runners and every server that deploys code. Anything older than 2.10 doesn't get the malware policy, and anything older than 2.11.0 (or 2.2.31 on the LTS line) misses the October 29 security fixes. Go straight to 2.11.1, which fixes a bug in 2.11.0 that could stopcomposer updatecold.composer self-updatefixes it, unless the server uses a distro package, in which case update it there. - Commit
composer.lockand deploy withcomposer install. Production should never runcomposer update. The lock file records exact commits and is what makes a build reproducible. - Read lock file diffs. When a PR changes
composer.lock, look at what moved. A patch release of a localization package that suddenly adds an autoloaded file is the kind of thing a human notices. - Stay off
dev-*branches. Keepminimum-stabilityatstable. Dev branches are mutable by design, so immutability doesn't cover them, and the cooldown policy won't either. All eight packages in the May 23 campaign were dev branches. - Keep an explicit
allow-pluginslist. Composer only runs plugins you've allowed. An API client that wants to become a plugin, like the poisoned intercom package, should never be on that list. - Run
composer auditin CI. Since 2.10 it fails on malware and on packages with security advisories. Make that failure stop the build. - Don't forget the JavaScript side. If your PHP project runs
npm install, those packages can run lifecycle scripts too. In CI,npm ci --ignore-scriptsis a reasonable default if your build doesn't need them. - Limit what installs can reach and read. Build steps shouldn't have production secrets in their environment or open internet access to anywhere. Snyk points out that an egress allowlist would have stopped the laravel-lang payload's second-stage download.
- Turn on the cooldown. It shipped in Composer 2.11. Composer's team suggests a few hours to a day, since a longer period also delays security fixes. Use
--no-cooldownfor a fix you need right away, andignorefor your own packages. - If you were exposed, rotate everything. If a host installed an affected version, assume every secret the PHP process could read is gone: database passwords, API keys, OAuth client secrets, cloud keys. Rebuild the box, don't clean it.
The caveats
Lock files don't freeze time. A composer install reproduces the lock, but the next composer update rewrites it with whatever the registry says now.
Scanners lag. Version numbers alone couldn't prove whether a laravel-lang install was affected, because the bad tags were later pointed back at clean code. Check install dates and indicators, not just the current version.
Your own accounts count. If you publish packages, turn on MFA on GitHub and Packagist. Packagist says mandatory MFA is coming.
That's not a reason to stop using open source. It's a reason to know what's in vendor/ and how it got there.
Where AI agents fit in
If an agent can run shell commands in your repo, it can run composer require. I let agents do a lot of the typing in my AI coding setup, but new dependencies get a human look before they're committed. An agent will happily add a package because a blog post mentioned it, and it won't check who maintains it.
What I still do by hand
The tooling now handles known malware and re-tagged versions. What it can't do is decide whether a package belongs in your project at all. Fewer dependencies means fewer maintainers you're trusting with your production secrets.
Need a hand?
Want your PHP dependencies checked?
Current Composer (2.11.1 as of October 30) blocks known malware, and Packagist now refuses re-tagged stable versions. That only helps if every laptop, CI runner and server actually runs it. What no tool can decide is whether a package belongs in your project at all. That takes a human reading lock file diffs, and I'm a little too happy doing it.
How I handle it:
- Find every Composer and every lock file. Versions on laptops, CI and deploy servers, plus a look for leaked tokens in
composer.lockand its history. - Tighten the setup. Deploys from a committed lock with
composer install,minimum-stabilityat stable, an explicitallow-pluginslist,composer auditin CI, and a sensible cooldown. - Trim and review. Packages you don't need come out, and lock file diffs get read before they're merged.
The aim is fewer surprises in vendor/ and a build you can trust. I can't promise a package you depend on will never be compromised, but I'll tell you what I'd tighten first and why. If you have a PHP or WordPress project that hasn't had its dependencies looked at in a while, get a free quote.
Caleb Dorris, senior software developer in Nashville
FAQ
What is a Composer supply chain attack?
It's when an attacker gets malicious code into a PHP package you install with Composer, usually by taking over a maintainer's GitHub or Packagist access. The code then runs on your machine, in CI or in production when the package installs or loads.
Does Composer block malware now?
Yes. Since Composer 2.10, released May 28, 2026, versions that Packagist has flagged as malware can't be installed through update or require. They also fail composer install from an existing lock file, and composer audit fails on them by default.
Is composer.lock enough to protect me?
It helps a lot, but it isn't enough on its own. It pins exact commits for composer install, but composer update refreshes from the registry. It also does nothing about a bad version you locked before anyone flagged it, unless you're on Composer 2.10 or later.
What is the Composer cooldown policy?
It's a setting that holds back package versions published within a period you choose, like 7 days, during composer update and require. It shipped in Composer 2.11.0 on October 29, 2026, and stays off until you set a period. Private Packagist offers an organization-wide version.
Sources
- Packagist blog, "An update on Composer & Packagist supply chain security"
- Packagist blog, "Composer 2.10 Release"
- Composer docs, "Config" (policy and cooldown)
- GitHub, composer/composer pull request #12692, "Add cooldown policy config"
- Socket, "Mini Shai-Hulud Spreads to Packagist: Malicious Intercom PHP Package"
- Snyk, "Laravel Lang Supply Chain Advisory"
- The Hacker News, "Packagist Supply Chain Attack Infects 8 Packages"
- GitHub, Composer 2.11.0 release notes (October 29, 2026)
- GitHub, Composer 2.11.1 release notes (October 30, 2026)
- GitHub, composer/composer pull request #13184, "Skip package validation for platform packages from the PlatformRepository"
- Composer changelog, 2.2.31 LTS (October 29, 2026)
- Packagist blog, "Composer 2.11" (October 29, 2026)
- GitHub advisory GHSA-fm2w-xgf2-f9fh, inline repository HTTP headers written into composer.lock
- GitHub advisory GHSA-c968-33g3-wh7p, installed.json package names escaping the vendor directory
- Composer download page (channels and LTS policy)
Need a web developer in Nashville?
PHP, WordPress and Next.js. Free quote, and I usually reply within a couple of hours.
caleb@calebdorris.com and call or text (615) 914-1073