Laravel-Lang Supply Chain Attack 2026: How 700+ Composer Tags Got Hijacked and How to Defend Your Stack
On May 22, 2026, an attacker with push access to the laravel-lang GitHub organization rewrote every git tag across four popular Composer packages inside a single 15-minute window. By the time researchers from StepSecurity, Aikido, and Snyk had published advisories the next morning, more than 700 tagged versions of laravel-lang/lang, laravel-lang/attributes, laravel-lang/http-statuses, and laravel-lang/actions resolved to a malicious commit that pulled a 5,900-line credential stealer into any project that ran composer install against a cold cache.
I am writing this as someone who has shipped Laravel applications for the better part of a decade. Across the 50+ projects we have delivered at Warung Digital Teknologi β ERP systems, point-of-sale software, hotel management suites, and our own Photography Studio Manager β Laravel is by far the most common backend in our stack. When I read the first Hacker News write-up at breakfast on May 23, my first move was to grep every project under ~/code for laravel-lang/ in composer.json. That took six minutes. The remediation, for anyone who had been pulling the latest tags in CI, takes considerably longer.
This article walks through what happened, how the attack abused Composer and Packagist tag resolution, and the specific defenses I have now baked into our own pipelines. If you ship PHP for a living, treat this incident as a forcing function β the same trick works against any GitHub-hosted Composer package today.
What actually happened
The four affected packages are third-party localization helpers. They are not maintained by the official Laravel team; they sit in the laravel-lang GitHub organization and are widely depended on for translation files, validation message dictionaries, HTTP status messages, and a small actions helper. Snyk and BleepingComputer report combined Composer downloads in the hundreds of millions over the lifetime of the packages, which is part of what makes this incident so consequential.
Here is the sequence, reconstructed from the StepSecurity, Aikido, and Mend advisories:
- Sometime before May 22, an attacker obtained push permission to the
laravel-langGitHub organization. The exact intrusion vector has not been published as of this writing, but the most likely candidates are a stolen maintainer token, a phished GitHub session, or a compromise of a maintainer's machine. - The attacker created a fork of each target repository under an account they controlled and committed a single malicious change: an injected
src/helpers.phpwired into Composer'sautoload.filesentry, plus an obfuscated payload loader. - Within a 15-minute window, the attacker rewrote every existing tag in the upstream repositories to point at commits in the attacker-controlled fork. Because GitHub permits a tag to reference any commit reachable from a fork of the same repository, Packagist faithfully accepted the rewritten tags and republished them.
- Any developer or CI runner that fetched the affected packages from a cold cache during the exposure window received the malicious code, regardless of whether they had pinned to
"^15.0","1.2.3", or any other specific tag.
The payload itself is the part that should keep PHP shops awake. The injected helpers.php fires on every request the moment Composer's autoloader runs. It reconstructs the command-and-control hostname from a byte array β researchers at securityonline.info and Aikido identified it as flipboxstudio[.]info β pulls down a second-stage PHP file, and executes it in memory. The second stage is a 5,900-line credential stealer organized into fifteen modules. Targets include AWS, GCP, and Azure metadata service tokens, Kubernetes and HashiCorp Vault secrets, GitHub and GitLab CI tokens, SSH keys and known_hosts files, .env contents, browser-stored credentials, password manager vault files, crypto wallet seeds, and Slack/Discord/Telegram tokens.
Why pinning to a version did not save you
The most common misconception I have seen on developer forums this week is "I pinned to 1.2.3 in my composer.json, so I'm fine." That is incorrect, and it is worth understanding why.
Composer resolves "laravel-lang/lang": "1.2.3" by asking Packagist for the metadata associated with that tag in the upstream repository. Packagist in turn asks GitHub. If the tag has been rewritten upstream, the new commit is what Packagist will hand back. Your composer.json constraint says which version to install; it does not by itself guarantee that the version's contents are the ones you originally vetted.
What does protect you is composer.lock β specifically the source.reference SHA recorded for each package. If you ran composer install (not composer update) from a lockfile generated before May 22 and your Composer cache already contained the original commit, you got the original code. If your cache was cold β typical for CI builds, Dockerfile RUN composer install layers without cache mounts, or fresh developer onboarding β Composer fetched the rewritten tag from upstream and you received the payload, lockfile or not.
This is the part that I think is genuinely surprising to PHP developers. We have been telling each other for years that "composer.lock makes your dependencies reproducible." That is true only as far as the SHA is concerned. The lockfile is a record of what should be installed, not a content-addressable archive of what was installed. The actual bytes still come from the upstream Git remote at install time, and if the remote has been tampered with, only the SHA mismatch will catch it β and Composer does not, by default, fail loudly when a tag's SHA changes between installs.
Detection: am I actually compromised?
If you ran any composer install or composer update that touched an affected package between May 22 and the time you read this, assume exposure until you can prove otherwise. The StepSecurity, Snyk, and Mend writeups all converge on a similar verification recipe, and I have adapted it for our own use.
1. Inventory. From your project root:
grep -rl "laravel-lang/" --include="composer.json" --include="composer.lock" .
2. Check the source SHA in composer.lock for each affected package:
jq '.packages[] | select(.name | startswith("laravel-lang/")) | {name, version, sha: .source.reference}' composer.lock
3. Verify the SHA against upstream branch history. Clone the upstream repository fresh from github.com/Laravel-Lang/<package>, then:
git log --all --format="%H %an %ae %s" | grep <sha>
If the SHA is not reachable from any branch in the upstream repository, or if the commit author resolves to a placeholder like Your Name <[email protected]> (a pattern flagged by multiple advisories), the install was poisoned.
4. Inspect vendor/laravel-lang/<package>/src/helpers.php on any deployed host. The legitimate packages do not ship a helpers.php with autoload-files wiring. If the file exists and contains obfuscated byte arrays or eval-style constructs, you have the malicious build on disk.
5. Check your egress logs. Any outbound DNS or TCP traffic to flipboxstudio.info from a build runner or web tier between May 22 and now is a confirmed indicator of compromise. The Aikido writeup lists additional fallback domains; check their advisory for the current full list before assuming you are clean.
If you find a compromised install, what do you actually do?
I want to be direct here, because vague incident-response prose is worse than useless when you are scared. If your verification turns up evidence of the poisoned commit on a production host or in a CI run that touched production credentials, the secrets that machine had access to should be treated as breached. That means:
- Rotate every credential the affected host could read. AWS/GCP/Azure keys, database passwords stored in
.env, third-party API keys, Stripe and other payment processor keys, mail provider keys, GitHub deploy keys, SSH keys for outbound deploys, any OAuth refresh tokens, and any encryption keys β Laravel'sAPP_KEYin particular. Without rotatingAPP_KEY, any session cookies and signed URLs issued before the breach remain forgeable. - Revoke and re-issue CI tokens. GitHub Actions
GITHUB_TOKENvalues are scoped per-run and expire, but any long-lived PATs, deploy keys, or secrets stored in your CI provider should be rotated. If you use HashiCorp Vault or AWS Secrets Manager, audit the access logs for unusual reads from build IPs. - Force a logout of every user. If you rotate
APP_KEY, sessions invalidate naturally. If you cannot rotateAPP_KEYsafely in your deployment, at minimum truncate the sessions table and require re-login. - Pin to known-good SHAs. In
composer.json, replace the version constraint with an explicit commit reference, or β more sustainably β fork the package to a repo you control and depend on the fork until the upstream maintainers issue audited replacement tags. - Notify your users where required. Depending on your jurisdiction (GDPR, UK DPA 2018, Indonesia's PDP Law, US state breach laws), you may have a legal duty to disclose. This is where I will repeat the disclaimer at the top: coordinate with counsel.
CISA's general supply chain compromise guidance (linked at the end of this article) and the NIST SP 800-161r1 software supply chain risk framework are the two reference documents I would pull up before drafting any external comms.
Preventive controls I am adding to our stack
I have spent the last 36 hours hardening our internal CI for the seven aggregator sites we operate, plus our active client Laravel projects. Here is what I am now treating as non-negotiable, based on the specific failure mode this incident exposed.
Egress allowlisting in CI
The single control that would have completely defeated this attack is an outbound network allowlist on build runners. The first-stage helpers.php is harmless until it can phone home to fetch the second-stage payload. If your build container can only reach packagist.org, repo.packagist.org, github.com, your own artifact registry, and your container registry, the malicious package installs but never executes its real payload β and your scanners get a chance to catch it before deploy.
I am using a small iptables OUTPUT chain on our self-hosted runners. GitHub-hosted runners have a similar story via step-security/harden-runner, which is the same project that broke this incident publicly.
SHA pinning, not version pinning
For dependencies that change rarely (utility libraries, locales, status-message dictionaries β exactly the kind of package laravel-lang is), pinning to an exact commit SHA via Composer's dev-<branch>#<sha> syntax and aliasing it removes the tag-rewrite attack surface entirely. The cost is that Dependabot and Renovate will not pick up new versions automatically. For low-churn dependencies, that is a feature.
Integrity verification in CI
Run composer install --no-cache --no-scripts in a verification job that compares the produced vendor/ tree's hashes against a known-good snapshot. The Composer team is actively discussing first-class content-addressable installs, but until that lands, a simple find vendor -type f -exec sha256sum {} \; diff against a baseline catches tag rewrites on day one.
Restrict what CI runners can read
The reason this payload was so damaging is that build runners typically have access to everything β production database credentials, cloud keys, signing keys, the lot. That is a separate failure mode from the supply chain attack itself. NIST SP 800-204D recommends per-job credential issuance with short TTLs. We are moving to OIDC-federated cloud credentials with 15-minute lifetimes; the breach radius of any future build-time compromise drops dramatically.
Subscribe to the right feeds
This story broke on Mastodon and the StepSecurity blog hours before it hit The Hacker News and BleepingComputer. If you ship Laravel, follow:
- GitHub Advisory Database with package ecosystem filters
- CISA Known Exploited Vulnerabilities Catalog (RSS feed available)
- The Snyk and Sonatype vulnerability feeds, both free at the entry tier
- The official Laravel blog for first-party advisories β note that the official Laravel team distanced itself from
laravel-langwithin hours, since the packages are not theirs
The broader lesson
I will close with the part of this story that I think gets underplayed in most write-ups. The Laravel-Lang attack is not a Laravel problem. It is not a Composer problem. It is a structural property of how every modern package manager that resolves tags from a remote VCS works β npm, RubyGems, Maven, PyPI, all of it. The 2024 XZ Utils backdoor showed the same shape against the OS package world. The 2022 colors.js and faker.js incidents showed it against npm. We are going to see more of this, not less.
The defensive posture worth adopting now is the assumption that your dependency tree contains, at any given moment, at least one package whose latest release is compromised. Design your CI and your production credentials around that assumption: short-lived secrets, egress allowlists, content-hash verification, and the discipline to read your lockfile diffs the same way you read your application code diffs. None of these controls are exotic, but most teams skip them because the threat felt theoretical.
As of May 24, 2026 β two days after the rewrite β Packagist has revoked the malicious tags and the laravel-lang maintainers have begun the slow process of re-tagging audited releases. By the time you read this, the immediate fire may be out. The structural problem will not be. Treat this week as the prompt to fix what you have been meaning to fix.
FAQ
Q: I only use Laravel's first-party packages. Am I exposed?
Not directly through this incident, since the affected packages live in the third-party laravel-lang organization, not the official laravel organization. Run the inventory grep anyway β many community starter kits and translation tooling list laravel-lang/lang as a transitive dependency.
Q: Does composer audit catch this?
Once a CVE or GHSA advisory is published for a specific version, composer audit will flag it. As of the May 23 disclosure, GitHub Security Advisories for the affected packages have been published; running composer audit against an updated database will now warn on poisoned installs. It does not, however, replace the SHA verification step.
Q: My production cache had the old SHAs. Am I in the clear?
Likely yes, provided you can prove no composer install ran from a cold cache during the exposure window. Verify by inspecting the on-disk vendor/ tree on every host and matching SHAs against a pre-May-22 lockfile.
Q: Should I migrate off the affected packages entirely?
That is overreaction. The maintainers were the victims here, not the perpetrators. Once they publish audited replacement tags signed with a re-issued GPG key, the packages are no riskier than any other dependency. The lesson is in your CI hardening, not your require list.
- StepSecurity β Laravel-Lang Supply Chain Attack: Every Tag Across Multiple Composer Packages Rewritten
- The Hacker News β Laravel-Lang PHP Packages Compromised to Deliver Cross-Platform Credential Stealer
- Aikido Security β Supply Chain Attack Targets Laravel-Lang Packages with Credential Stealer
- Snyk β Laravel Lang Supply Chain Advisory
- CISA Cybersecurity Alerts & Advisories
- NIST SP 800-161r1 β Cybersecurity Supply Chain Risk Management Practices
Found this helpful?
Subscribe to our newsletter for more in-depth reviews and comparisons delivered to your inbox.
Related Articles