Lessons Laravel Developers Should Learn from the laravel-lang Attack
On 22–23 May 2026, four packages in the laravel-lang organization — laravel-lang/lang, laravel-lang/attributes, laravel-lang/http-statuses, and laravel-lang/actions — were silently backdoored through a single account compromise. Every existing git tag across all four packages was rewritten within a 90-minute window. Within six hours, 5,561+ downstream repositories had received the poisoned code.
The injected payload was a helpers.php file wired into autoload.files in composer.json. Because autoload.files executes code on every PHP request immediately after installation, the backdoor ran without any action from application code. It exfiltrated AWS keys, GitHub tokens, Stripe secrets, SSH keys, .env files, JWTs, Kubernetes secrets, and crypto recovery phrases to flipboxstudio.info.
This is a technical walkthrough of the exact failure modes the attack exploited, and the concrete steps you can take today to prevent the same class of attack against your Laravel projects.
Prerequisites
- Composer 2.4+ for
composer audit; 2.9+ foraudit.block-insecure - Laravel 10, 11, or 12
- CI pipeline access (GitHub Actions or equivalent)
Why Tag Pinning Failed
Most developers assume that pinning a package to a specific version ("laravel-lang/lang": "^6.3") means Composer will always install the same code. That assumption is wrong when it comes to git tags.
Composer resolves a version constraint to a git tag. It then fetches the commit that the tag currently points at. If a maintainer (or attacker) rewrites the tag to point at a different commit, Composer will pull the new commit — there is no version mismatch, no checksum failure, no warning.
The laravel-lang attack exploited this precisely. The attackers rewrote every stable tag, so any project running composer update or even a fresh composer install with an outdated composer.lock received the backdoored code.
The only tamper-evident anchor Composer has is composer.lock. The lockfile stores the resolved commit SHA alongside the dist hash. If you commit your lockfile and deploy with composer install (not composer update), Composer will refuse to install a package whose content hash no longer matches.
# Safe — installs exactly what composer.lock specifies, verifies hashes
composer install --no-dev --optimize-autoloader
# Dangerous in CI/production — resolves constraints fresh, rewrites composer.lock
# Do not use in automated pipelines
composer update
This is the single most impactful change you can make: never run composer update in CI or production. Run it locally, review the diff in composer.lock, and commit the result as a deliberate change.
Check Your Lockfile Right Now
If you use any of the four affected packages, check immediately:
grep -E '"laravel-lang/(lang|attributes|http-statuses|actions)"' composer.lock
If any of those packages appear and you ran composer update or a fresh install between 22–23 May 2026 UTC, treat the host as compromised. Rotate all secrets that were accessible from the build environment.
Indicator of compromise: outbound HTTP/S connections from your build machines or containers to flipboxstudio.info.
The autoload.files Threat Surface
The attack vector — autoload.files — deserves specific attention. Composer's autoload.files section lists PHP files that are included on every request via the generated vendor/autoload.php. Unlike PSR-4 classes that only load when referenced, these files execute unconditionally as part of the autoloader bootstrap.
Legitimate packages use autoload.files for global helper functions (Laravel itself uses it for Illuminate/Support/helpers.php). Attackers use it for the same reason: guaranteed execution with no trigger required.
When auditing third-party packages, pay attention to any package that declares autoload.files. You can audit yours:
# List all packages using autoload.files
cat composer.lock | php -r '
$lock = json_decode(file_get_contents("php://stdin"), true);
foreach ($lock["packages"] as $pkg) {
if (!empty($pkg["autoload"]["files"])) {
echo $pkg["name"] . "\n";
foreach ($pkg["autoload"]["files"] as $f) {
echo " " . $f . "\n";
}
}
}
'
Review the output. Any unfamiliar file in an autoload.files entry is worth reading.
Running composer audit in CI
Composer 2.4 introduced the audit command. It reads composer.lock and checks every installed package against the PHP Security Advisories Database. This should be a required step in every CI pipeline.
# Run audit — exits non-zero if advisories are found
composer audit
# JSON output for machine parsing
composer audit --format=json
Composer 2.9 (released November 2025) went further and introduced audit.block-insecure, which defaults to true. This blocks composer update operations when any installed package version has a known security advisory.
{
"config": {
"audit": {
"block-insecure": true,
"block-abandoned": false
}
}
}
One important caveat: composer audit checks packages against published CVEs. The laravel-lang attack went undetected by composer audit for the first several hours because no CVE existed yet. Dependency scanning catches known vulnerabilities. It does not catch novel supply chain attacks where a backdoor has not yet been catalogued. This is why egress controls matter.
Egress Controls in GitHub Actions
The most effective defence against credential exfiltration from a compromised package is preventing the exfiltration itself. GitHub Actions workflows run in network-accessible environments where every installed package can open outbound connections.
StepSecurity's Harden-Runner adds network-level egress policy to your workflow jobs:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: step-security/harden-runner@v2
with:
egress-policy: block
allowed-endpoints: |
packagist.org:443
repo.packagist.org:443
github.com:443
api.github.com:443
- uses: actions/checkout@v4
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Run security audit
run: composer audit
With egress-policy: block, any outbound connection not in the allowlist is blocked. A package attempting to reach flipboxstudio.info would have been silently dropped before it could exfiltrate anything. Add this to your workflows before you need it.
Static Analysis with Ward
For deeper code-level inspection, Ward is a Go binary security scanner built specifically for Laravel projects. It runs 42+ built-in rules across secrets, injection, XSS, debug configuration, crypto, auth categories, and queries the OSV.dev vulnerability database in real time.
# macOS
brew install eljakani/tap/ward
# Or download binary from GitHub releases
# github.com/Eljakani/ward
# Scan your project
ward scan /path/to/laravel-project
# Export SARIF for GitHub Code Scanning integration
ward scan . --output sarif > ward-results.sarif
# Export JSON for automation
ward scan . --output json > ward-results.json
Ward does not require Composer or access to a running deployment. The SARIF output integrates with GitHub's Code Scanning feature, surfacing findings as pull request annotations.
The Three CVEs You Must Patch
The May 2026 attack was the headline, but three other vulnerabilities are equally urgent:
CVE-2025-54068 — Livewire RCE (Critical, CISA KEV)
Affects Livewire v3 through v3.6.3. An attacker can bypass the APP_KEY-signed checksum and achieve unauthenticated remote code execution via deserialization during component hydration. This does not require knowledge of your application key. CISA added it to the Known Exploited Vulnerabilities catalog, meaning it is being actively used in the wild.
# Check your Livewire version
composer show livewire/livewire | grep versions
# Upgrade
composer update livewire/livewire
# Target: 3.6.4 or later
CVE-2026-39976 — Laravel Passport Authentication Bypass (CVSS 7.1)
Affects Passport 13.0.0 through 13.7.0. TokenGuard does not verify whether the JWT sub claim belongs to a user or a client. A machine token issued via client_credentials can authenticate as a real user when integer IDs collide between the clients and users tables. Fixed in Passport 13.7.1.
This only triggers if you use EnsureClientIsResourceOwner middleware and have Passport::$clientUuids set to false. Check whether your Passport configuration matches this profile before concluding you are safe.
CVE-2025-27515 — Laravel File Validation Bypass (Moderate)
Affects wildcard file validation rules (e.g., 'files.*') in Laravel Framework. Fixed in 10.48.29, 11.44.1, and 12.1.1. If your application accepts file uploads and validates them via wildcard rules, update your framework version.
Common Mistakes to Avoid
Running composer update in CI. This rewrites composer.lock and may pull in newly poisoned versions. CI must always run composer install.
Not committing composer.lock. Without a committed lockfile you cannot detect whether a dependency changed between deployments.
Suppressing audit warnings. COMPOSER_NO_AUDIT=1 silences real CVE alerts. Triage and resolve each advisory instead.
Trusting autoload.files entries blindly. These files execute on every request — review them before including any package in production.
Not rotating after a compromise. If your build environment ran the poisoned packages, rotate all accessible secrets immediately: AWS keys, GitHub tokens, Stripe secrets, and your APP_KEY.
Packagist's Response and Remaining Gaps
Following the attack, Packagist implemented stable version immutability — published stable releases can no longer be overwritten, directly addressing the git tag rewrite vector.
This protection applies only to packages on Packagist. Private forks, VCS repositories in composer.json's repositories section, and path-installed packages are not covered. If your project pulls from any non-Packagist source, integrity verification is your responsibility.
Planned improvements include FIDO2 MFA for maintainers, organizational package ownership verification, and SLSA provenance with Sigstore attestations — none are in place today for all packages.
Verification Checklist
Run through this after making the changes above:
-
composer.lockis committed to version control - CI pipeline runs
composer install, nevercomposer update -
composer auditruns in CI and exits non-zero on findings -
audit.block-insecure: trueis set incomposer.json(Composer 2.9+) - Egress controls are applied to CI workflow jobs
- Livewire is at 3.6.4 or later
- Laravel Passport (if used) is at 13.7.1 or later
- Laravel Framework is at 10.48.29, 11.44.1, or 12.1.1+ for file validation fix
-
autoload.filesentries in third-party packages have been reviewed - Network logs checked for any connections to
flipboxstudio.info
For a broader look at Composer security hardening, dependency scanning with Enlightn, and application-level scanning with StackShield, read the Laravel Security Guide: Supply-Chain Risks, Composer, and Application Scanning.
If you need Laravel development in Mumbai, Mumbai Web Designer builds production-grade Laravel applications.
Top comments (0)