Blocking Malware Downloads for Every Composer Version in Private Packagist

Blocking Malware Downloads for Every Composer Version in Private Packagist

This is the next post in our supply chain security series, following the supply chain security update, the Composer 2.10 release, and the recent post on closing Composer's download fallback paths.

Composer 2.10's dependency policy framework is a substantial step forward for PHP supply chain security. It removes flagged malware versions from the dependency resolution pool used for composer update, require or create-project, and blocks them during composer install even when they have already made it into a lock file, and surfaces them through composer audit. The Packagist.org malware feed, powered by Aikido, gets every Composer 2.10 user installing from Packagist.org rapid malware blocking by default.

Composer 2.10 covers the case where a developer is on a current Composer version with the default policy configuration. Two practical gaps remain:

  • A project can disable the malware policy in its own composer.json with "config": {"policy": {"malware": {"block": false}}.
  • Composer versions older than 2.10 have no concept of dependency policies at all and continue installing flagged versions normally.

Let’s look at an example. First, here is Composer version 2.10 blocking malware from being installed:

Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Your lock file does not contain a compatible set of packages. Please run
composer update.
  Problem 1
    - Package evil/badpkg v1.0.0 (in the lock file) was not 
    loaded, because it was flagged as malware (see
    https://packagist.org/packages/evil/badpkg/filter-lists/malware/)
    reason: malware. To ignore filters for this package, add the package to
    the "policy.malware.ignore" config. To turn the feature off entirely,
    you can set "policy.malware.block" to false.

But what happens if you attempt to install the same dependencies on a less recent version of Composer, without any protections through Private Packagist in place?

Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Package operations: 1 install, 0 updates, 0 removals
  - Installing evil/badpkg (v1.0.0): Extracting archive
Generating autoload files

Supply chain attacks move fast. The Composer binary in a project's CI image often lags months or years behind the latest release. A developer can easily accidentally run an old version of Composer locally. An AI agent may decide to install and run an old version of Composer autonomously. So a business cannot rely on every employee and every automated system operated within the organization to update to the latest version of Composer for thorough protection against a fresh supply chain attack. This is the gap we set out to close.

Repository-level blocking, regardless of client

Starting now, Private Packagist refuses to serve dist/artifact files for malware-flagged versions, regardless of which Composer version is asking for them. Packagist.org is unable to offer this functionality, as it does not host any of the dist/artifact files itself.

When Composer requests a dist file for a flagged version, whether through composer install against an existing lock file, or through composer update or composer require, Private Packagist responds with HTTP 410 and a JSON body identifying the package and version and explaining that the download was refused because the version has been flagged as malware. Neither a fresh resolution nor a lock-file replay can result in a flagged version being installed, on any Composer client. A Composer 2.4 baked into a long-running CI image gets the same protection as a freshly-installed Composer 2.10.

Packagist.org removes malware versions where necessary after manual review, and those deletions propagate to Private Packagist. Manual review takes time though. Sometimes hours, during which projects can still install the flagged version. The Aikido feed runs continuously and flags versions much sooner. Repository-level blocking on Private Packagist hooks into the feed directly, so once a version is flagged the refusal takes effect immediately, with no manual intervention required.

Malware blocking on dist/artifact download is enabled by default for all organizations, new and existing. You don't need to opt in. There is a switch in your organization's security settings if you ever have reason to disable it.

Prerequisite: Closed fallback paths

Repository-level blocking on its own is not quite the full story. Composer attempts package downloads in a sequence: the preferred Private Packagist URL first, then the upstream dist URL (if advertised), then a source-code checkout. If Private Packagist refuses the dist with a 410, Composer can still obtain the same malicious code from GitHub or another upstream repository: silently, on any client that has not yet adopted the Composer 2.10 source-fallback default.

The previous post in this series covers this in detail, including the two Private Packagist options that close these fallback paths at the metadata level:

  • Legacy insecure package download fallback (disabled by default): strips upstream dist URLs from package metadata, making Private Packagist the only download URL.
  • Hide source code checkout URLs from Composer: removes source URLs from package metadata, so even older Composer clients and projects that explicitly enable source fallback have no source to fall back to.

Combined with repository-level malware blocking, these two options give you a single line from Private Packagist to the developer's machine and CI, with no detours and no version of Composer that can route around the policy.

Now let’s see what happens on the same project with malware, only this time attempted to be downloaded through Private Packagist.

Package operations: 1 install, 0 updates, 0 removals
  - Downloading evil/badpkg (v1.0.0)
Warning from repo.packagist.com: Version 1.0.0.0 of evil/badpkg has been flagged as malware and can no longer be installed from Private Packagist.

In CurlDownloader.php line 671:
  The "https://repo.packagist.com/my-org/dists/evil/badpkg/1.0.0.0/ad8747ea706950793a76612b7982c7fc95b5864f.zip" file could not be downloaded (HTTP/2 410 ):

As you can see, it no longer matters which Composer version you use, as Private Packagist prevents Composer from downloading the malware-infected dist file.

Configuration and defaults

Defaults differ for new and existing organizations:

  • New organizations get the strict configuration by default: malware download blocking on, legacy fallback off, source URLs hidden for all packages from mirrored third-party repositories.
  • Existing organizations get malware download blocking on by default. The two fallback-closing options remain opt-in, since dropping source URLs more broadly can affect existing workflows (most commonly, open-source contributors who install from source locally with --prefer-source).

We recommend that existing organizations review the two fallback-closing options. The combination of all three is what gives you defense in depth across every Composer version.

A note for customers using suborganizations: the configuration UI lives on the parent organization. Suborganizations display the inherited values but cannot edit them directly, and changes on the parent propagate down. A per-suborganization override is on the roadmap. Get in touch if that scenario applies to you.

What this means for you

You should still upgrade to Composer 2.10 across all your projects as soon as you can. But where you haven’t yet, Private Packagist now refuses to serve flagged malware versions anyway. This helps the decade-old Composer pinned in a CI image, the version an AI agent installs on a whim, and the project that configured a malware policy opt-out in its composer.json

The Composer 2.10 dependency policy framework is the most flexible way to handle malware, advisories, and abandoned packages. It's the foundation for the security features we're building next. But you no longer have to wait for that rollout in your organization to be protected. Repository-level blocking on Private Packagist covers the gap today, and closing the two fallback paths provides defense in depth from Private Packagist to every machine and pipeline you run.

We're continuing to strengthen supply chain security across Private Packagist. Over the next days we'll cover even more of the Composer behaviors and Private Packagist features that build on the 2.10 release.