Back to Blog

Laravel-Lang Supply Chain Attack: Every Tag Across Multiple Composer Packages Rewritten to Steal CI Secrets

On May 22, 2026, an attacker with push access to the Laravel-Lang GitHub organization rewrote every git tag across multiple popular Composer packages within a single 15 minute window. Anyone running composer update or installing fresh against laravel-lang/http-statuses, laravel-lang/actions, or laravel-lang/attributes now pulls a payload that exfiltrates CI secrets to a typosquatted attacker domain. StepSecurity confirmed end to end exploitation in an isolated runner and has filed security issues in all four repositories.
Varun Sharma
View LinkedIn

May 22, 2026

Share on X
Share on X
Share on LinkedIn
Share on Facebook
Follow our RSS feed
Table of Contents

Summary

On May 22, 2026, a single threat actor compromised four popular Composer packages maintained by the Laravel-Lang organization. Rather than publishing a new malicious version, the attacker rewrote every existing git tag in each repository to point at a new malicious commit. The rewrites started at 22:32 UTC against laravel-lang/lang (the flagship Laravel translations package, with 502 tags) and finished by 00:00 UTC against laravel-lang/actions. All four repositories share the same fake author identity, the same modified files, and the same payload behavior, which makes them almost certainly the work of one actor using one compromised credential with org wide push access.

The malicious commits add src/helpers.php to the Composer autoload.files map. This means simply requiring vendor/autoload.php, which every Laravel and Symfony application does on startup, fires the payload. The payload reaches out to flipboxstudio.info, drops a hidden PHP loader and an ELF binary into /tmp, exfiltrates runner environment data, and then deletes its own artifacts from disk to frustrate forensics.

We confirmed end to end exploitation by detonating laravel-lang/http-statuses v3.4.5 in an isolated GitHub Actions runner protected by Harden-Runner in audit mode. The other three packages share identical commit structure but have not been detonated yet. We expect they behave the same way.

We have reported this to the Laravel-Lang maintainers by opening a security issue in each of the four affected repositories:

Affected versions

All four packages have had every git tag rewritten. There is no safe version to pin to today other than a pre 2026-05-22 commit SHA that you can independently verify against a local clone or the Packagist mirror.

GitHub tags page for Laravel-Lang/http-statuses. Every tag, including versions that are years old, shows a creation time of "1 hour ago", because every tag was force pushed to a new commit during the attack window.

laravel-lang/lang (all 502 tags, the flagship Laravel translations package; this was the first and largest target in the campaign):

  • 15.29.5 at commit a5ea2e8fa92ccf29cdb1d2dadbeb27722b2bff37
  • 15.29.1 at commit 50ac0db454d19234c835716f297bbc5363c0a25c
  • 2.0.4 at commit c45764e70285146da37025cd8601a921ab8a7eda
  • 1.0.2 at commit a9f8d88cf98e35988d3d0fd6d79547f980853041
  • Every other tag follows the same pattern. Rewrites span 2026-05-22 22:32 UTC through 23:24 UTC.

laravel-lang/http-statuses (every tag from v1.0.0 through v3.4.5):

  • v3.4.5 at commit bba2e443dc7ff1f8704f52a5375383e3f4f643b8
  • v3.4.0 at commit 26c233e1a0d4fd2331e8e0f175e18f8eed904aa3
  • v3.0.0 at commit db0c3ef246103fd0f6c318e0d48f26b5289044c3
  • v2.0.0 at commit 9ee599d248cc322fa26054694a83a1f4558cc716
  • v1.0.0 at commit 6b1d5782a8c8c199d070857802d39bfe609eb6f2
  • Every other tag in between follows the same pattern.

laravel-lang/actions (all 46 tags from 1.0.0 through 1.12.2):

  • 1.12.2 at commit 556d2b335d4d6d92139822017ee461b668afe375
  • 1.10.0 at commit 722cee67326d932e7f71ba3438f62a255d779aa9
  • 1.0.0 at commit ad24b980db8f0dca50ccb3ba6badb3c2331e0ef4
  • Every other tag follows the same pattern.

laravel-lang/attributes (all 86 tags):

  • v2.4.1 at commit d59561727927117e65b35f0183cae131baad19fe
  • 2.6.0 at commit 1713b19cbf609cb101ff5e216be41f7224269082
  • 2.5.0 at commit daa5212264bb73fb39fe7a36618b62717dc564a5
  • Every other tag follows the same pattern.

Because the rewrites moved every tag rather than introducing a new version, projects that pin to a version range (^3.4, ~2.0, *, and so on) and run composer update will resolve to a poisoned tag. Projects whose composer.lock already pins a commit SHA from before 2026-05-22 are safe as long as they only run composer install against that lockfile, and as long as the locked SHA is not one of the imposter commits listed above.

Indicators of compromise

Network indicators:

  • C2 domain: flipboxstudio.info (a typosquat of the legitimate flipboxstudio.com)
  • Stage 1 fetch: GET https://flipboxstudio.info/payload
  • Stage 2 exfiltration: POST https://flipboxstudio.info/exfil

Filesystem indicators (these files self delete a few seconds after the payload runs, so they are most likely to be visible only on a live runner or in a filesystem snapshot taken during execution):

  • /tmp/.laravel_locale/<12 hex chars>.php, a hidden directory containing a hidden PHP loader
  • /tmp/.<8 hex chars>, an ELF binary with no file extension

Process indicators (visible in ps even after the dropper files are deleted, because the binary continues to run from memory):

  • An orphaned php process with ppid=1
  • An orphaned unnamed ELF process with ppid=1, executing from a now deleted path under /tmp

Git indicators:

  • Commit author name Your Name and email you@example.com on every tagged commit in the affected repositories
  • Commit timestamps spanning 2026-05-22 22:32 UTC through 2026-05-23 00:00 UTC
  • Each commit modifies only two files: composer.json and src/helpers.php

How the attack was carried out

Composer supports three autoload modes in composer.json. Two of them, psr-4 and classmap, are lazy: a class file only loads when its class is referenced. The third, files, is eager: every listed file is required the moment vendor/autoload.php is required.

The attacker added src/helpers.php to the autoload.files map in each compromised package. Because every Laravel application calls require __DIR__.'/vendor/autoload.php' on startup, and because Symfony, PHPUnit, and most other PHP frameworks do the same, the payload runs the moment any consumer of the package boots. No class instantiation, no method call, no special trigger is required.

The malicious commit on Laravel-Lang/http-statuses, attributed to "Your Name". GitHub's banner ("This commit does not belong to any branch on this repository") is the tell that the commit is unreachable from any branch and was force pushed only as a tag target. The diff in composer.json adds a new "files" entry to autoload, pointing at src/helpers.php.

The contents of the added src/helpers.php are short and self contained. The file fetches a second stage from $sh/payload (where $sh resolves to https://flipboxstudio.info), writes it to a hidden filename under the system cache directory using bin2hex(random_bytes(6)) as the filename, and executes it with a backgrounded php "$f" > /dev/null 2>&1 &. The Windows branch in the same file uses a WSH WScript.Shell object to achieve the equivalent detached execution.

The dropper code in src/helpers.php. The curl block fetches the stage 2 payload, bin2hex(random_bytes(6)) generates the random hidden filename, and @exec("php \"$f\" > /dev/null 2>&1 &") detaches the loader so it survives the autoload return.

Harden Runner Detections

The remainder of this section is the raw evidence captured by Harden Runner during the detonation of laravel-lang/http-statuses v3.4.5 in run 26318135547. Times are UTC. PIDs come from the runner's process table.

Process tree. The autoload step we ran is at the top. Each subsequent sh wrapper exists only to background its child and detach it. The result is two implant processes adopted by init, while the dropper files on disk are removed within two seconds of first execution.

00:17:45.972  php (pid 2804)                              [workflow step: Autoload package]
                  invoked: php -r "require 'vendor/autoload.php'; echo 'autoload complete';"

00:17:47.012      └─ sh (pid 2805)
                       invoked: sh -c "php /tmp/.laravel_locale/f3e2c293172f.php > /dev/null 2>&1 &"

00:17:47.013           └─ php (pid 2806, ppid=1)            [reparented to init]
                            invoked: php /tmp/.laravel_locale/f3e2c293172f.php

00:17:48.129               └─ sh (pid 2813)
                                invoked: sh -c "nohup /tmp/.480dc608 > /dev/null 2>&1 &"

00:17:48.130                    └─ nohup -> /tmp/.480dc608 (pid 2814, ppid=1)  [reparented to init]

00:17:49.134                        ├─ rm (pid 2816)
                                    │      invoked: rm -f /tmp/.laravel_locale/f3e2c293172f.php
00:17:49.135                        └─ rm (pid 2817)
                                           invoked: rm -f /tmp/.480dc608

Two facts in this tree are worth highlighting. First, PIDs 2806 and 2814 both end up with ppid=1, which means they were reparented to init when their immediate shell parent exited. That is what makes them survive the workflow step boundary and what causes Harden Runner to attribute their network calls to infra rather than to the runner user. Second, the entire compromise (initial autoload to self deletion of both artifacts) takes 3.16 seconds.

Network calls. Two HTTPS requests to the attacker C2 domain, attributed to the correct PIDs:

00:17:46.055   pid 2804 (workflow php)   GET   https://flipboxstudio.info/payload
00:17:47.156   pid 2806 (orphan loader)  POST  https://flipboxstudio.info/exfil

The GET /payload call is what fetched the two staged files written to /tmp. The POST /exfil call carried whatever the loader harvested from the runner environment (most likely $GITHUB_TOKEN, the contents of $GITHUB_ENV, secrets from $RUNNER_TEMP/_runner_file_commands/, and anything readable from /proc/<runner pid>/environ). We did not capture the request body because Harden Runner was in audit mode for this run.

What an unprotected runner sees. Outside of Harden Runner, a defender investigating this incident after the fact has very little to work with. The two dropped files are gone within 3 seconds. The two implant processes have no parent in the workflow's process tree because they were reparented to init. The only durable artifact is the outbound DNS and TLS traffic to flipboxstudio.info, which on a hosted GitHub Actions runner is usually not captured anywhere. This is why egress monitoring is the most reliable detection for this class of payload.

You can view the Harden Runner insights here: 

https://app.stepsecurity.io/github/actions-security-demo/compromised-packages/actions/runs/26318586189?tab=network-events&jobId=77482953054

Harden Runner Network Events for the detonation workflow run. The expected baseline destinations (github.com, repo.packagist.org, api.github.com, packagist.org) are all green. The last row shows the call from the "Autoload package to trigger runtime payload" step to flipboxstudio.info, flagged red as Attack Blocked, which is what stops the payload from reaching its C2.

Recovery steps

If you depend on any of the four affected packages:

  • Stop running composer update, and stop running composer install without a known good lockfile, for any project that depends on laravel-lang/lang, laravel-lang/http-statuses, laravel-lang/actions, or laravel-lang/attributes. laravel-lang/lang is the most widely used of the four (the core Laravel translations package) and is the highest priority to triage.
  • Inspect composer.lock for these packages. If the locked commit SHA is one of the imposter commits listed above, or if the lockfile was regenerated on or after 2026-05-22 22:32 UTC, treat the project as compromised.
  • If you ran a Composer install of these packages on or after 2026-05-22 22:32 UTC, treat every secret accessible to that environment as compromised. This includes CI provider tokens, cloud provider credentials (AWS, GCP, Azure), GITHUB_TOKEN and any other GitHub PAT, container registry credentials, deploy keys, database credentials, and any application secret available in environment variables. Rotate all of them.
  • Audit the runners and developer machines where the install ran. Check ps auxf for orphaned php and unnamed ELF processes with ppid=1. Check /tmp for the artifacts listed above. They may still be present if you check soon enough after the install.
  • If you use Harden-Runner, search your detections for HTTPS calls to flipboxstudio.info across your tenant. Any hit confirms a compromised workflow run.
  • Add flipboxstudio.info to any egress blocklist, firewall rule, or DNS sinkhole you control.

If you are a Laravel-Lang maintainer, or you operate a similar org:

  • Force update every tag in the four affected repositories back to its original commit. If the local reflog is gone, the Packagist dist mirror retains the original tarballs, and any user with a clone made before 2026-05-22 has the original SHAs.
  • Audit every account, app, and personal access token with push access to the Laravel-Lang organization. Revoke and rotate everything. Re enable 2FA enforcement.
  • Notify Packagist so the affected versions can be flagged or yanked.
  • Audit the rest of the organization for similar rewrites. The attacker had the access and the time to compromise more than four repos, and we may only have spotted the loudest cases.

Blog

Explore Related Posts