PHP at Scale #22
Supply Chain Security in 2026 - What PHP Can Finally Learn From npm’s Bad Year
Welcome to the 22nd edition of PHP at Scale. I am diving deep into the principles, best practices and practical lessons learned from scaling PHP projects - not only performance-wise but also code quality and maintainability.
2.6 billion weekly downloads for packages that an attacker managed to take over in September 2025. Reason: a fake two-factor email to the maintainer.
Outcome: thankfully minimal, as it was caught within two hours by the ecosystem. And there are more examples, including self-replicating worms that spread across hundreds of popular repositories in recent months. Not once, but twice! And in the most sobering case, attackers shipped malware carrying *valid* cryptographic provenance, defeating the exact defence the industry spent two years building.
This is why I’m breaking from the usual rotation this month. No architecture patterns, no API design - just one topic that deserves a full issue: software supply chain security, and the wave of attacks that has reshaped the threat model faster than anything I’ve seen in my career.
PHP got off relatively lightly - but “we run Composer, not npm” is not a security strategy, and the people who maintain Composer know it. The good news: they’ve moved fast, and Composer 2.10 is a serious response. The honest news: there are still gaps, and some of the best defenses are things *you* turn on, not things you wait for. Let’s get into it.
I’ll start with the attacks themselves - how they worked, and what they teach - then move to the concrete changes worth making.
Inside the Shai-Hulud npm worm
Most of you saw the headlines, so this is a reminder, not a lecture - but it sets up everything else.
September 2025: a maintainer got phished through a fake npmjs.help two-factor email, and malicious versions of chalk, debug and 16 other packages shipped with a combined 2.6 billion downloads a week. It then evolved into Shai-Hulud, the first self-propagating npm worm: a malicious install script harvests npm tokens, GitHub PATs and cloud keys, then uses the stolen token to auto-republish itself across every package that maintainer owns. No human in the loop - 500+ packages, and a bigger second wave in November.
Two things matter. Attackers stopped impersonating popular packages and started compromising the real ones - trust isn’t a defense when the trusted thing is poisoned. And the target moved from laptops to CI runners, where the real secrets live.
And it already reaches us - the May 2026 devdojo/wave campaign hid its malicious postinstall in the package.json inside Packagist packages. The PHP supply chain, hit through the npm side of a PHP project. Almost every “PHP” app I work on has a node_modules too.
Provenance proves who built it, not that it’s safe - the TanStack case
This is the one that should worry senior people, because TanStack did everything right - OIDC trusted publishing, signed SLSA provenance, 2FA on every account - and still shipped malware across 42 packages in May 2026, every malicious version carrying a valid provenance attestation.
The weak point was the release pipeline, not the maintainers.
A pull_request_target workflow checked out fork code; an attacker poisoned the GitHub Actions cache, and the release run restored it, extracted the OIDC token from the runner and published. The provenance was genuine - it correctly attested that the official pipeline built the artifact. It just couldn’t attest that the pipeline hadn’t been hijacked.
Keep this in mind before PHP finishes shipping the same features: provenance proves “built by the official pipeline,” not “the code is safe.” A poisoned pipeline will produce valid attestations for malware all day long. Which is exactly why I’d call zizmor (further down) a must-have for any open-source repo on GitHub Actions - it catches this class of misconfiguration automatically. There’s no drop-in equivalent for GitLab CI, but the same principle holds there: pin your include: refs and lean on GitLab’s built-in SAST.
---
Looking at how this evolves, I’d assume that someday a package I rely on will get compromised. Methods change and defences improve, yet the risk never fully goes away. So the goal isn’t to feel safe - it’s to shrink the window and the blast radius.
We should all be using dependency cooldowns
If you do one thing after reading this issue, do this one. It has the highest return on effort on the list, and you can have it today without waiting for anyone.
The insight is almost embarrassingly simple: nearly every malicious release gets caught and pulled within days. Refuse to install anything younger than three to seven days, and you sidestep the majority of these attacks for free - the only cost is slightly less-fresh dependencies, and nobody needs a non-security patch within 24 hours of release. pnpm defaults minimumReleaseAge to one day in v11; npm, Yarn, Bun, uv, pip and Poetry all have it. Composer is the one still behind - milestoned for 2.11, key reserved, not here yet. But you don’t have to wait: Renovate has minimumReleaseAge (set ”3 days”), Dependabot has cooldown, and in both, security updates bypass the wait, so real CVE patches still land fast.
One honest caveat: a cooldown is only as trustworthy as the date it checks. If the tool reads the author-controlled release date (git tag, composer.json), an attacker who has already compromised the maintainer can just backdate it and walk straight through. That’s an open question for Composer’s implementation right now (issue #12793 wants a registry-controlled publish_date that the author can’t set). Until that timestamp is registry-side, treat cooldowns as a strong speed bump against automated attacks - not a guarantee against a targeted one.
Malware blocking and dependency policies in Composer 2.10
This is a serious, well-designed response, and it shipped fast. Composer 2.10 (end of May 2026) is the first version with native malware blocking.
Packagist.org now imports Aikido Security’s malware feed, and flagged versions are pulled out of the resolution pool - you can’t require or update into them. The detail I like most: the check also runs on composer install, so a version that was clean when your composer.lock was written but flagged later will fail your next install rather than slip in. composer audit fails on malware by default now, too. It all lives in a unified config.policy block (advisories, abandoned, malware, each with block / audit / ignore).
Keep the two layers distinct: the CVE path (composer audit, since 2.4) covers disclosed vulnerabilities on slow timelines; the malware path is for outright malicious code that must be blocked in hours. 2.10 adds the second alongside the first. The caveat: it’s enforced client-side, so an old Composer binary in a stale CI image doesn’t get it. That’s the gap Private Packagist closes server-side, refusing to serve the flagged dist (HTTP 410) to every client. (If you want behavioral detection rather than a known-bad list, Socket added PHP support in February - it’s what caught devdojo/wave - but that’s a paid layer, not a replacement for the free thing Composer now does.)
So make sure you’re on Composer 2.10 everywhere, and consider Private Packagist and/or Socket if you need more security layers in your company.
An update on Composer & Packagist supply chain security
So what should you expect from 2.11 besides cooldowns? Nils Adermann (Composer’s co-creator) and Igor Benko laid out a roadmap that’s refreshingly honest about where PHP still trails.
Two things already landed in 2026. A transparency log - a public record of ownership changes, tag modifications and 2FA events, modelled on Certificate Transparency: it doesn’t prevent an attack, it makes one impossible to do quietly, so monitors catch the “ownership change then sudden release” pattern fast. And stable-version immutability - you can no longer silently re-tag a released version, closing a vector PHP had open for years.
For 2.11 specifically, beyond cooldowns, the one to know is removal of the source-fallback path. Composer used to silently fall back to a git source checkout when a dist download failed - which could fetch a version you thought was blocked. 2.10 deprecated it; 2.11 removes it. What’s in your lock is what you get, full stop. Longer-term plans include: mandatory MFA, FIDO2-backed staged releases, SLSA + Sigstore provenance.
Hardening GitHub Actions workflows
Bring it home to something every PHP repo has: a CI pipeline. Since CI is where the worms hunt for secrets, that’s where your leverage is - and Sebastian Bergmann (yes, the PHPUnit one) wrote up how he hardened PHPUnit’s own workflows, which makes it directly applicable.
Obviously, a to-do list for an open-source project differs from a closed-source one, but it’s always worth taking a look and implementing at least some of the improvements. And you don’t have to eyeball it all by hand - tools like zizmor scan your workflow files and flag exactly these issues (unpinned actions, dangerous triggers, over-broad token permissions) automatically, so it’s worth wiring one into CI.
While you’re there, harden Composer in CI too: composer install --no-scripts --no-plugins --prefer-dist for production, plus an explicit config.allow-plugins allowlist. And one genuine PHP advantage worth saying out loud: Composer does not run install scripts from your dependencies the way npm runs every package’s postinstall. Only your root project’s scripts run, and since 2.2 any third-party plugin must be explicitly allowlisted before it can execute code. That one design decision shuts down the most common npm worm vector.
Read it with your own pipeline open - it’s an afternoon that closes the exact hole that owned TanStack.
The role of SBOMs: from visibility to vulnerability response
One last shift - from attackers to regulators. The EU Cyber Resilience Act lands, and it’s the angle your boss and/or your clients’ procurement teams will care about.
The timeline: it’s in force now, 11 September 2026, the vulnerability/incident reporting obligations kick in (24-hour early warning, 72-hour notification), and by 11 December 2027, the full set - secure-by-design, a machine-readable SBOM, vulnerability handling, security updates for the support lifetime - becomes mandatory for products sold in the EU. Penalties reach €15M or 2.5% of global turnover. Open-source stewards get lighter obligations, but any commercial vendor shipping OSS components inside their product is fully on the hook.
Is an SBOM actually useful, or just paperwork? It genuinely helps, and incident response is the killer use case. Log4Shell (December 2021) is the proof: teams with a component inventory answered “where do we run log4j, at what version?” in minutes; teams without one spent days grepping and still weren’t sure. An SBOM piped into OWASP Dependency-Track turns every future chalk/debug-style disclosure into a query instead of a fire drill. The PHP starting steps are a one-afternoon job: composer require --dev cyclonedx/cyclonedx-php-composer, then composer CycloneDX:make-sbom --output-format=JSON --output-file=sbom.json (guide). Generate it in CI, feed Dependency-Track, start with top-level dependencies.
---
Two more attack classes, grouped together because they share something uncomfortable: no policy file, cooldown or malware feed reliably stops either. Both come down to human judgment instead.
Slopsquatting: AI code hallucinations fuel supply chain attacks
The attack that genuinely didn’t exist two years ago - and the reason this is a 2026 issue.
LLMs confidently invent package names that don’t exist. Attackers harvest those hallucinated names, pre-register them as malware, and wait for the next developer - or autonomous agent - to paste the suggestion and run composer require. The numbers make it real: open-source models hallucinate a package name about 21.7% of the time, and 43% of those names recur on every re-run of the same prompt - stable enough to squat reliably. One researcher’s empty proof-of-concept package got 30,000+ downloads in three months.
This ties straight to the AI-tooling thread I keep returning to. As your team lets Claude, Copilot or Cursor scaffold code and run the install commands, this is the new failure mode - and the defence is mostly human: someone verifies the package exists and is the real one before it lands, cooldowns blunt freshly-squatted names, and composer.lock review catches the diff. AI in the loop raises the bar for “look at what you’re installing.” It doesn’t lower it.
The patient insider - the XZ Utils backdoor
The other end of the spectrum, and the one this issue doesn’t catch. XZ Utils (2024) is the reference case - an attacker spent two years as a “helpful” contributor, pressured a burned-out maintainer into handing over commit rights, and landed a backdoor through legitimate commits. No cooldown, malware feed or provenance check helps when the trusted maintainer is the threat. Tooling raises the cost of the smash-and-grab. It doesn’t solve trust, and nothing fully does.
---
There’s no single fix here, but there is a clear priority order, and most of it is configuration you already know how to do. Stop thinking of your dependencies as static text in composer.json, and start thinking of every composer update and every CI run as a moment where someone else’s code executes with your privileges.
The “by Monday” list, in order of return on effort:
1. Turn on cooldowns in Renovate/Dependabot now (security updates still bypass them).
2. Get Composer ≥ 2.10 everywhere and run composer audit as a CI gate.
3. Add a config.allow-plugins allowlist and use --no-scripts for production installs.
4. Pin your GitHub Actions to SHAs and lock down token permissions. Same for external docker images.
5. Enable 2FA on every Packagist account that can publish.
6. Make a human verify any AI-suggested package before composer require.
7. Generate an SBOM - you’re both CRA-ready and Log4Shell-ready.
Which of these have you actually turned on?
PS. If you forward one thing from this issue to your team, make it the cooldowns. Cheapest insurance in software.
PS2. I’d love to hear some improvements you implemented to make your project(s) more secure. Share them in comments or e-mail me, and I will gather them for my next release.
---
Why is this newsletter for me?
If you are passionate about well-crafted software products and despise poor software design, this newsletter is for you! With a focus on mature PHP usage, best practices, and effective tools, you’ll gain valuable insights and techniques to enhance your PHP projects and keep your skills up to date.
I hope this edition of PHP at Scale is informative and inspiring. I aim to provide the tools and knowledge you need to excel in your PHP development journey. As always, I welcome your feedback and suggestions for future topics. Stay tuned for more insights, tips, and best practices in our upcoming issues.
May thy software be mature!

