A developer needs a fix from a contrib module, so they open a terminal inside DDEV and run composer update. The container can reach an SSH agent and a hosting token. Composer downloads a few packages and writes a new lock file.
Nothing about that looks unusual. That is the problem.
The recent wave of supply chain attacks did not exploit Drupal. It exploited the moment a developer or a build changes dependencies with credentials in reach. The Drupal Security Team has been clear about the boundary. Its remit is code hosted on Drupal.org, and site owners are responsible for third-party libraries.
The rest of the dependency tree is ours.
We develop locally in DDEV and use Dependabot in GitHub for update pull requests. Here is how we think about protecting a Composer-based Drupal project.
The risk arrives when dependencies change
These attacks arrive with a dependency change, and a Drupal project gives them three places to run.
The first is Composer plugins. In April 2026, an attacker moved an existing intercom/intercom-php tag to a malicious commit. The malicious commit turned the library into a Composer plugin that stole credentials during install.
The second is autoload.files. In May 2026, attackers rewrote historical tags on four laravel-lang packages. The payload was registered in autoload.files, so it ran on every PHP request.
The third is npm lifecycle scripts. The Shai-Hulud worm spread through npm install scripts and the npm tokens it stole. A later campaign hid a postinstall hook in the package.json of PHP packages, aimed at projects that also run JavaScript tooling. Many Drupal themes run npm tooling, so the theme's package.json belongs in the same review.
Each vector needs its own control.
Allow plugins by name and treat the lock file as code
Commit composer.lock, and have builds and deploys run composer install rather than composer update. An install reproduces the commits already recorded in the lock file. The same behavior protected laravel-lang users whose lock files predated the attack.
Keep drupal/core-recommended as the core requirement, because it pins core's own dependencies tightly.
A common piece of generic advice is to install with --no-plugins, but on Drupal that breaks the build. Both composer/installers and the core scaffold plugin are Composer plugins. Without them, modules do not land in web/modules/contrib and scaffold files are never placed.
The control that fits Drupal is allow-plugins. By default it allows no plugins, and the drupal/recommended-project template already ships a short list of named packages. Keep that list explicit, and never set it to true.
Then review lock file diffs as carefully as code. Three changes deserve a second look:
- a package whose
typebecomescomposer-plugin - a new plugin appearing at all
- a changed source reference on an unchanged version number
An unexpected plugin prompt is a code review event, not a formality.
Let Composer and npm block what they already know is bad
Newer package managers now do much of this work by default. Composer 2.9 blocks updates to versions with known security advisories. Composer 2.10 adds malware blocking that also applies to composer install, so a malicious version that slipped into a lock file fails the next build instead of installing quietly.
That malware data comes from a Packagist.org feed, so it covers core and its dependencies but not contrib from packages.drupal.org.
Versions with advisories can still be installed from an existing lock file, though. The audit therefore belongs in the build:
composer install --no-dev --no-interaction --no-progress composer audit --locked
Drupal advisories reach composer audit because packages.drupal.org publishes advisory data for the projects it hosts. When an advisory does not apply to a site, record an exception with a reason in Composer's policy configuration instead of removing the audit step.
That also retires two older tools. Drush 12.3.0 removed pm:security in favor of composer audit, and Composer's built-in blocking replaces roave/security-advisories.
On the theme side, npm 12 blocks dependency lifecycle scripts unless the root package explicitly allows them. Treat that allowScripts list like allow-plugins, and install with npm ci against a committed package-lock.json.
Teams that want a review step on every dependency change can watch Laravel Vet. Despite the name, it works with any Composer project. It is still in beta and requires PHP 8.4, and we have not tested it with Drupal.
Wait a week, except for security releases
We recommend a 7-day waiting period for routine Composer and npm updates, with security releases exempt and applied the same day.
The timing supports it. The malicious intercom-php tag was live for under two hours, and Packagist says malicious versions are typically pulled or detected within hours. The first Shai-Hulud worm kept publishing new malicious versions for about two days in September 2025, so a week leaves margin. For a long-lived Drupal site, a week of delay on routine updates costs little.
The exemption matters just as much.
Drupal shipped core security releases rated highly critical in May 2026 and critical in October 2026. Both times, the Security Team warned that exploits might be developed within hours or days. A waiting period must never delay those updates.
For Composer packages from Packagist.org, Dependabot already waits 3 days by default. In dependabot.yml, a 7-day cooldown looks like this:
version: 2 updates: - package-ecosystem: "composer" directory: "/" schedule: interval: "weekly" cooldown: default-days: 7
The cooldown applies only to version updates, never to security updates. That exemption only helps when Dependabot recognizes the advisory. GitHub's reviewed advisories cover Drupal core, often late, but not contrib modules from packages.drupal.org.
Contrib security releases therefore arrive as ordinary version updates. The cooldown does not hold them back either. Dependabot's code reads release dates from a field that packages.drupal.org does not publish, so the 7-day window covers core and other Packagist.org packages, not contrib.
Contrib needs a manual path. Watch the Drupal security advisories and apply those releases directly, and check release dates on routine contrib updates.
The package managers can enforce the same window. npm 11.10 added min-release-age, so min-release-age=7 in the theme's .npmrc sets that window for npm.
Composer's native cooldown is merged but unreleased, and it is planned for Composer 2.11 as policy.cooldown.period set to "7 days". Its documentation measures age from published-time, which packages.drupal.org publishes, so it should cover contrib too. Neither npm nor Composer recognizes security fixes, so urgent fixes need a per-package exemption.
Install-time code can read whatever is in reach
Assume anything running composer update or npm install can read everything in that environment. The payloads above went looking for cloud credentials, .env files, SSH keys, and CI secrets.
Run npm inside the DDEV container rather than on the host, so a script sees the container and not a developer's home directory. Then review what the container and the build runner can actually see, including forwarded SSH agents, hosting tokens, and local settings files with real credentials.
Keep long-lived credentials out of jobs that change dependencies, and give build tokens the narrowest permissions the job needs. Shai-Hulud used the GitHub tokens it found to push workflows into every repository it could reach.
Finally, protect the accounts that publish code. Anyone who maintains packages should turn on two-factor authentication for Drupal.org, GitHub, Packagist, and npm.
Supply chain security is a maintenance habit
None of these controls is exotic. Commit the lock file and install from it. Allow plugins by name, and gate builds on composer audit. Wait a week for routine releases, and apply security releases the same day. Keep credentials away from the moment dependencies change.
Supply chain security is not a scanner we add to a project once. It is how we treat every dependency change.
Read This Next
- Mercury Editor 3: A Better Visual Editing Experience for Drupal
- Extending Editoria11y with Custom Content Checks in Drupal
- Freedom with the form Attribute: More Flexible Drupal Views Exposed Filters
- The Cost of Waiting: Why CMS Maintenance Belongs in Every Website Budget
- Who Owns the Icon? Building Author-Friendly Icon Systems in Drupal