Last week, an attacker did something most supply chain compromises don't do. They didn't publish a new version of a package. They rewrote every git tag on the ones that already existed.
According to StepSecurity's incident report, on May 22, 2026, somebody with push access to the Laravel-Lang GitHub organization rewrote 502 git tags on laravel-lang/lang inside a fifteen-minute window. They did the same to laravel-lang/http-statuses and laravel-lang/attributes. Socket put the total closer to 700 once historical tags were counted.
Composer trusted the tags. Composer always trusts the tags. That's the bug.
What Actually Happened
The mechanism deserves a careful read. Most supply chain attacks publish a new malicious version. Think left-pad, event-stream, the colors.js sabotage. Security tools catch new versions because new versions are events. New advisories ship. CI fails loudly.
This attack inverted that. Every tag in the affected repos still looked like the trusted tag you've installed for years. But the underlying commit those tags pointed at had been replaced with one that included a poisoned helpers.php. That file was wired into composer's autoload.files entry, so it ran on every PHP request the moment the package was installed. According to BleepingComputer's writeup, the dropper pulled down a Windows binary called DebugElevator from a third-party domain and began exfiltrating AWS keys, GitHub tokens, Slack tokens, Stripe secrets, SSH keys, .env files, JWTs, Kubernetes secrets, Vault tokens, and (yes) cryptocurrency recovery phrases.
Symbols inside the binary referenced both a username, "Mero," and the string "claude," which security researchers took as a sign of AI-assisted development. The payload was roughly 5,900 lines of PHP organized into fifteen specialist collector modules. This was not a smash-and-grab.
Packagist removed the malicious versions and temporarily unlisted the affected packages on May 23.
Why composer.lock Alone Isn't Enough
Most Laravel developers will tell you composer.lock is the law. It is. It records exact versions, content hashes, and source URLs. If your lock file was committed before the attack and nobody on your team ran composer update against the affected packages between May 22 and the takedown, you are probably safe on that codebase.
"Probably" is the operative word. composer.lock pins the package version and a content hash for archives downloaded from Packagist. It does not pin the underlying git commit SHA on packages installed from source. If anyone on your team ran composer update against an affected package during the window, the lock file got rewritten with the malicious commit, and Composer wrote it back as if nothing was wrong. The hash matched. The version matched. The code was poison.
That's the part worth sitting with. The trust model isn't "the maintainer is good." It's "the tag is immutable." Git tags are not immutable. They are pointers, and any pointer with write access behind it can move.
What to Check Today
If you run anything on Laravel or PHP that ever touched laravel-lang/lang, here's the practical audit.
First, search every composer.lock you can reach for the affected packages:
find . -name composer.lock -exec grep -l "laravel-lang/" {} +
For each match, run composer audit on the project. Packagist's security advisory database now includes the bad versions, and the audit command reads composer.lock and checks every installed package against the PHP Security Advisories Database.
Second, if you find a hit, don't just composer update. Pin to a specific commit hash of a known-good version, or to a version installed before May 22 from an archive you trust. Composer supports dist source as well as source, and you want the published archive from before the attack window, not a fresh git clone of the tag.
Third, rotate everything that ever touched the affected box. AWS keys. GitHub PATs. Slack tokens. Stripe restricted keys. SSH keys. Anything in .env that resembles a credential. If the payload ran, it grabbed all of these. Treat the credential surface as compromised even if you can't confirm exfiltration succeeded.
Fourth, audit your CI. The credential stealer binary was Windows-targeted, but the PHP autoloader trigger ran everywhere. If your CI does a fresh composer install per build on Linux runners, the binary dropper failed there. If you have Windows runners, or developer machines that pulled the package in the window, those hosts are the ones at meaningful risk. SecurityWeek's coverage makes this distinction clearly.
A Note on What This Means for Composer
Composer 2.9 ships automatic blocking of known-vulnerable packages during dependency resolution, which is a meaningful upgrade over the older audit-after-the-fact model. SymfonyCasts wrote up how the new default works. That helps for future incidents once advisories ship. It does not help during the window between compromise and disclosure, which in this case was about twenty-four hours.
The bigger lesson is that the Composer ecosystem has been leaning on tag immutability as a load-bearing assumption, and that assumption was always optimistic. GitHub allows force-pushing tags by default. Packagist caches against the tag, not the commit. The fix isn't on individual developers. You cannot grep your way out of "the maintainer's account got owned." It needs to come from infrastructure: signed tags, content-addressed package caches, and Packagist treating tag rewrites as a security event by default.
Until that lands, the practical defense for agencies and product teams running Laravel is a real maintenance posture. Not the marketing kind. The kind that audits composer.lock on a schedule, runs composer audit in CI, pins to commit hashes for high-risk packages, and treats credential rotation as a routine quarterly exercise rather than a panic move during an incident. Most maintenance plans don't actually maintain much. They patch when something breaks. This is the category of risk that breaks quietly, and you only find out months later when someone notices invoices from an AWS account you didn't know was still active.
Where Pixelworx Lands
We run Laravel sites for clients in production. We checked our composer.lock files the morning the advisory hit. None of our active projects use laravel-lang/lang directly, though a few pulled it transitively through other packages, which is exactly the kind of three-levels-deep risk every dependency advisory keeps pointing at. We rotated the relevant tokens anyway. It cost us an afternoon. It would have cost a great deal more if we'd been on an affected version.
The "Mero / claude" string in the binary is also worth a moment. AI is making attackers more productive in the same way it's making everyone else more productive. The fix isn't to keep AI away from your code. The fix is to keep the trust boundary clear about whose code is running, and to assume any author (human or otherwise) can be wrong or worse. We made the same argument about the offensive side of this trade-off two weeks ago. The defensive side looks identical.
If you're not sure whether your site is exposed, the audit is small and the downside of skipping it is large. If you want a second set of eyes on a Laravel codebase, that's what our maintenance and support work looks like in practice. If you're building net new on the TALL stack and want a partner that treats supply chain hygiene as a default rather than an afterthought, that's also us.
The Laravel ecosystem will be fine. Composer will be fine. The attackers shipped a clever exploit, the community responded inside a day, and the advisory pipeline did its job. But this one rewrote our mental model of what "version 1.5.3 of a package" actually guarantees. The honest answer is: less than we thought.