top of page

The Laravel-Lang Credential Stealer Never Touched the Official Repo. It Used GitHub Tags As Misdirection.

  • Writer: Patrick Duggan
    Patrick Duggan
  • 2 minutes ago
  • 4 min read

On May 22, 2026, a credential-stealing supply chain attack lit up the Laravel/PHP ecosystem. By May 23, security researchers at Aikido, Socket, and the Hacker News had published the dissection. The headline number is 700 — as in 700-plus version tags rewritten across three widely used packages in the Laravel-Lang organization. The number that matters more is zero, as in the number of malicious commits ever pushed to the official repositories. The attacker did not compromise the source code. The attacker compromised the addressing.



The trick: tags can point anywhere


GitHub supports a feature that, for fifteen years, was a quiet convenience for maintainers and a quiet land mine for everyone downstream: a Git tag in repository A can point at a commit that lives in a fork of A. The tag itself is metadata stored alongside the repo, but the commit it dereferences is a free pointer. If a tag named v3.4.0 in laravel-lang/lang references a SHA that exists only in a malicious fork of laravel-lang/lang, Composer — and most package consumers — will happily fetch and install the contents of that SHA without ever surfacing the fact that the code is not in the canonical history of the official repository.


That is what happened. The Laravel-Lang attacker built a fork of each target repository, committed a credential stealer into the fork, and then rewrote over 700 historical version tags inside the official repositories to point at SHAs that exist only in their fork. Anyone who ran composer install or composer update against any pinned version of laravel-lang/lang, laravel-lang/attributes, or laravel-lang/http-statuses during the attack window fetched the stealer. The official repository's commit log was clean the entire time. The blame view was clean. The pull request history was clean. The deception lived in the tag layer, which almost nobody monitors.



The payload: a fifteen-module collector


The fetched code is a roughly 5,900-line PHP credential stealer organized as fifteen specialist collector modules. It scrapes cloud metadata, environment files, database credentials, CI secrets, and developer keychains. It encrypts the harvested payload with AES-256, exfiltrates it to attacker-controlled infrastructure, then deletes itself to frustrate forensic recovery. The autoloader is the trigger — Composer loads it the moment the affected package is required, which means the steal happens during install, not at runtime, which means a CI pipeline that runs composer install on every build will exfiltrate every time the build runs.


This payload shape is not new. The novelty is the delivery vector. Past Pattern 38 supply chain compromises on our tracker have been pull-request-poisoning, dependency confusion, typosquatting, and outright maintainer-account takeover. The fork-pointed-tag mechanism is a fifth species. It looks more like the misdirection in a stage magic act than a code intrusion. The defender's eyes are watching the repository; the trick happens at the addressing layer that the repository visualization doesn't draw.



Why the existing detection stack misses it


GitHub's own security tooling, dependency-update bots, and most software composition analysis products surface changes by examining the contents of a tag at fetch time and comparing against a baseline. They do not, by default, audit whether a tag's referenced commit lives inside the canonical commit graph of the repository it claims to belong to. A tag pointing at a fork commit is a valid Git object. It looks fine. It checksums fine. Composer's lockfile records the SHA, and if a developer reviews the lockfile diff, they see a SHA change and assume the maintainer published a new version. That is the gap.


The defensive primitive that catches this is one line of logic per package: at install time, verify that the SHA referenced by the tag is reachable from a branch ref of the official repository. If the SHA exists only in a fork's history graph, refuse the install and surface the discrepancy. Composer does not enforce this today. Neither does pip. Neither does npm. The fork-pointed-tag attack works against every major package ecosystem that consumes Git tags as version anchors — which is essentially all of them.



What we are doing with this


DugganUSA's IOC index now carries the named repositories and the attacker fork URLs as observed-infrastructure records, with source-tagged provenance and Pattern 38 classification. We are not republishing the malicious SHAs because the canonical repos have been cleaned and Packagist has unlisted the affected versions, so further enumeration of dead pointers does not help defenders. What does help defenders is the structural rule: any CI pipeline that runs composer install during build should now treat unsigned version pins as a hostile-by-default surface until the package ecosystem catches up.


The wider lesson lands in our standing frame. Hard perimeter held — the official Laravel-Lang repositories were never compromised. Soft surfaces bled — the tag-addressing layer that everyone assumed was inside the perimeter turned out to be a free pointer that lived next to the perimeter. The asymmetric edge here belongs to the defender who treats the tag layer as adversarial-by-default. The defender who treats the repository name as trust burns themselves on this one.


Pin to commits, not tags. Audit reachability before install. Watch the tag layer the way you watch the dependency graph. The trick is fifteen years old. It just stopped being theoretical.




How do AI models see YOUR brand?

AIPM has audited 250+ domains. 15 seconds. Free while still in beta.


bottom of page