Composer 2.9: security blocking and repository policy

Understand why an update can now fail on an advisory, inspect the locked dependency graph, and review repository configuration as a trust decision.
A blocked update leaves a separate question about the lock
Composer 2.9.0 was released on 13 November 2025. Its release-candidate changelog introduced default security-advisory blocking during updates through audit.block-insecure. An affected version can be excluded before the solver writes a new lock file.
This article is for maintainers investigating a blocked dependency update. The reproduction uses Composer 2.9.0 on PHP 8.3.35, tested on 12 October 2026. It separates resolution failure, the existing lock and unavailable advisory data using a synthetic repository.
Inspect the graph that still exists
The fixture first creates a lock containing fixture/unsafe 1.0.0. Its local advisory endpoint reports the invented SYNTHETIC-001 for versions below 1.0.1. With the root requirement pinned to exactly 1.0.0, an update exits 2 and leaves the original lock bytes unchanged.
The script below displays the locked package and audits that lock. Against the affected version it still reports SYNTHETIC-001 and exits 1. The failed update prevented a new resolution; it did not remove or repair the version already recorded in composer.lock.
#!/bin/sh
set -eu
composer show --locked
composer audit --locked --format=json
Allow the fixed version without disabling blocking
Changing the synthetic root requirement from exactly 1.0.0 to ^1.0 allows the script below to select 1.0.1. The subsequent local audit contains no finding. The fixture changes the constraint while retaining the existing lock and the default blocking policy.
The same update script fails before that constraint change and succeeds afterward. Its --no-audit flag skips the post-update report but does not bypass resolution-time security blocking in Composer 2.9.0. --no-install and the disabled scripts/plugins keep this experiment focused on the lock graph.
I recommend that you identify which direct or transitive constraint prevents the fixed version before considering an exception. An exception should name the advisory, reason, owner and review deadline. Permanently disabling blocking for the whole project hides the distinction between an accepted risk and an unresolved update.
#!/bin/sh
set -eu
composer update --no-install --no-audit --no-plugins --no-scripts
Repository precedence changes which versions exist for the solver
Composer 2 repositories are canonical by default. Once a higher-priority repository contains a package, lower-priority repositories are not considered for it. This predates Composer 2.9; the new repository CLI command is a configuration convenience, not a new trust model.
In the reproduction, the first repository offers fixture/private 1.0.0 and the second offers 1.9.0 under the same ^1.0 requirement. Composer selects 1.0.0 while the first repository is canonical. Changing only that setting to non-canonical allows 1.9.0 from the lower-priority source.
I recommend that you review repository order, package filters and source locations alongside version constraints, especially for a private namespace. A higher version from a different source is still a source change. Do not put repository credentials in a committed URL.
An unavailable feed must remain an error
When the fixture advisory endpoint returns HTTP 503, Composer 2.9.0 audit exits 100 and reports the transport failure. The test checks both signals. It does not convert missing advisory data into an empty successful report.
Download the source and tests and run python3 frontend/scripts/verify-composer.py from the extracted folder. The runner builds checksum-verified historical Composer binaries, creates the graphs, serves a v2 repository on container loopback and checks the observed outcomes. It requires Docker, Python 3 and network access for the initial build.
All packages and advisories are invented. External networking is disabled during execution, so no Packagist or live vulnerability feed is consulted. The empty synthetic report after selecting 1.0.1 is not evidence about real package safety. HTTP is used only for container loopback, not as recommended repository transport. The tests do not install an application or establish API compatibility.
Reproduce the examples
Download the synthetic package repositories, six shell examples, pinned runtime and assertions. Requires Docker and Python 3; no repository account is needed.
Download the source and tests (.zip)
