Lange Zeit war Composers Umgang mit Security-Advisories informierend, nicht durchsetzend: composer audit gab nach einer erfolgreichen Installation eine sauber formatierte Tabelle mit Advisories aus, aber der Resolver selbst prüfte nicht, ob eine Version verwundbar war.
Wenn du eine harte Garantie wolltest, dass eine bekanntermaßen unsichere Version nicht in deinem vendor-Verzeichnis landen konnte, musstest du dich auf ein Drittanbieter-Paket verlassen, meist auf roave/security-advisories. Und darauf vertrauen, dass dessen Liste der Konfliktregeln aktuell war.
Composer 2.9 hat das geändert. Der Resolver selbst weigert sich nun, Versionen mit bekannten Advisories überhaupt in Betracht zu ziehen. Standardmäßig. Bei jeder Installation und jedem Update. Composer 2.10 wird dieselbe Maschinerie auf als Malware markierte Versionen ausweiten und dafür ein allgemeineres Konzept namens „Filter-Listen“ verwenden.
Dieser Artikel erklärt, wie die beiden Mechanismen tatsächlich funktionieren, wie sie sich zu den älteren Paketen verhalten, die sie ersetzen, und an welchen Stellen das neue Verhalten dich auf eine Weise treffen kann, wie es das alte, passive Audit nie getan hat.
Konfliktregeln als Sicherheitswerkzeug
Vor 2.9 bestand der Ansatz, verwundbare Installationen zu blockieren, in einem geschickten Missbrauch der bestehenden Konfliktauflösung von Composer. Zwei Repositories haben diesen Ansatz historisch geprägt.
FriendsOfPHP/security-advisories ist die Rohdatenquelle: eine gemeinfreie YAML-Datenbank mit PHP-Sicherheitsproblemen, eine Datei pro Advisory, organisiert nach Composer-Paketname. Jede Datei listet betroffene Branches und Versions-Constraints in Composers eigener Syntax (['>=2.0.0', '<2.0.17']). Der Contribution Guide beschreibt ausdrücklich die branches-Struktur, bei der jeder Branch ein time-Feld für den Zeitpunkt, an dem das Problem behoben wurde, und ein versions-Array mit den betroffenen Versions-Constraints enthält. Sie ist, wie in ihrer eigenen Dokumentation steht, nicht als maßgebliche Quelle gedacht. Sie ist ein praktischer Aggregator für Informationen, die anderswo leben (Hersteller-Advisories, CVE-Einträge, GitHub).
Roave/SecurityAdvisories macht aus diesen Daten etwas, womit der Composer-Resolver tatsächlich etwas anfangen kann. Das Paket liefert eine einzige composer.json, deren conflict-Abschnitt die FriendsOfPHP-Datenbank (zusammen mit Daten aus der GitHub Advisory Database) widerspiegelt und stündlich neu gebaut wird. Du installierst es als roave/security-advisories:dev-latest unter require-dev, und ab da scheitert jeder Versuch, eine betroffene Version hinzuzufügen oder auf sie zu aktualisieren, mit einem gewöhnlichen Fehler über „widersprüchliche Constraints“. Das Paket selbst enthält keinen Code: Sein ganzer Zweck ist es, als eine Menge von Konflikten im Abhängigkeitsgraphen zu existieren.
Dieser Ansatz hat echte Grenzen. Er greift nur bei composer require und composer update. composer install aus einer gültigen Lock-Datei tut absichtlich nichts, was für reproduzierbare Deployments korrektes Verhalten ist, aber auch bedeutet, dass eine Lock-Datei, die vor der Veröffentlichung eines Advisories gepinnt wurde, die verwundbare Version auf ewig weiter installiert. Es kann nur auf der Wurzelebene des Projekts verlangt werden. Es lässt sich nicht pro Advisory konfigurieren. Es gibt keinen „dieses hier bitte ignorieren, es trifft auf uns nicht zu“-Schalter. Und die Fehlermeldung, wenn sie ausgelöst wird, sieht aus wie ein gewöhnlicher Versionskonflikt und nicht wie eine Sicherheitsmeldung, was die Analyse langsamer macht, als sie sein müsste.
Melden ohne Durchsetzen
Composer hat seit Version 2.4 einen audit-Befehl. Er liest deine composer.lock, fragt für jedes installierte Paket die Advisory-API von Packagist ab und gibt einen Bericht aus.
Die Advisory-API von Packagist aggregiert mehrere Upstream-Quellen, darunter die FriendsOfPHP-Datenbank, die GitHub Advisory Database, den Security-Announce-Feed von Drupal und einige weitere. Advisories ohne CVE- oder GHSA-Kennung bekommen von Packagist eine synthetische Kennung mit dem Präfix PKSA-, damit sie konsistent referenziert werden können. Derselbe Datenstrom speist sowohl den audit-Befehl als auch, seit Composer 2.9, den unten beschriebenen Durchsetzungsmechanismus.
Automatic Security Blocking
Automatic Security Blocking, veröffentlicht in Composer 2.9, verschiebt die Durchsetzung von Advisories aus einem separaten Paket in den Resolver selbst. Der zugehörige Pull Request ist composer/composer#11956.
Der Mechanismus ist eine neue Stufe in der Pipeline der Abhängigkeitsauflösung, genannt SecurityAdvisoryPoolFilter. Composer baut einen Kandidaten-Pool aus jeder Version jedes Pakets auf, die die Constraints in der composer.json potenziell erfüllen könnte, und lässt dann einen SAT-Solver über diesen Pool laufen, um ein installierbares Set auszuwählen. Der neue Filter läuft zwischen diesen beiden Schritten: Nachdem der Pool gebaut wurde, aber bevor der Solver startet, fragt er jedes Repository nach Security-Advisories für die Pakete im Pool, gleicht sie mit jeder Kandidatenversion ab und entfernt jede betroffene Version. Der Solver sieht dann eine Welt, in der die verwundbaren Versionen schlicht nicht existieren.
Die ausführliche Ausgabe aus dem Pull Request macht die Stufen sichtbar:
Built pool. Running security advisory pool filter. Security advisory pool filter completed in 0.001 seconds Found 105 package versions referenced in your dependency graph. 1 (1%) were filtered away. Running pool optimizer.
Wenn das resultierende Solver-Problem nicht erfüllbar ist, weil jede Version eines benötigten Pakets herausgefiltert wurde, tut Composer nicht so, als wäre das ein allgemeiner Constraint-Fehler. Die Fehlermeldung sagt genau, was passiert ist:
Root composer.json requires doctrine/cache <=1.3.0,>=1.0.0,
found doctrine/cache[v1.0, v1.1, v1.2.0, v1.3.0] but these were not
loaded, because they are affected by security advisories.
To ignore the advisories, add ("PKSA-5g3y-77s3-wsh1") to the audit
"ignore" config. To turn the feature off entirely, you can set
"block-insecure" to false in your "audit" config.
Zwei Dinge sind zum Anwendungsbereich dieses Features erwähnenswert.
Erstens läuft er nur bei Befehlen, die den Abhängigkeitsgraphen auflösen: update, require, remove. composer install aus einer Lock-Datei ist davon unberührt, genau so, wie Christophe Coevoet es während des Reviews argumentiert hat: Filtern zur Install-Zeit würde die Reproduzierbarkeitsgarantie der Lock-Datei brechen, sobald ein Advisory erscheint, weil der Graph dann heimlich auf Maschinen neu aufgelöst würde, die ihn eigentlich nur wiedergeben sollen. Das ist eine bewusste Designentscheidung und kein Versäumnis.
Zweitens verlässt sich der Filter auf Advisories, die die konfigurierten Repositories zur Verfügung stellen. Für Packagist.org heißt das der aggregierte Feed, der FriendsOfPHP, GitHub, Drupal SA und den Rest abdeckt. Für private Composer-Repositories heißt es das, was das Repository selbst bewirbt.
Weil das Feature im Resolver lebt, ersetzt es das, was roave/security-advisories tat. Die Release Notes zu Composer 2.9 sagen es direkt: „Wenn du derzeit roave/security-advisories nutzt, um Pakete mit bekannten Schwachstellen zu blockieren, ersetzt dieses Feature es vollständig, und du kannst die Abhängigkeit zu diesem Paket entfernen.“
Konfiguration
Das Blockieren wird über einen neuen audit-Abschnitt im config-Objekt der composer.json gesteuert. Die drei wichtigsten Einstellungen sind:
-
block-insecure(Defaulttrue)
Auffalsegesetzt fällt das Verhalten auf das von 2.8 zurück, in dem Advisories zwar gemeldet werden, aber nicht blockieren. -
block-abandoned(Defaultfalse)
Opt-in, um Pakete zu blockieren, die auf Packagist als „abandoned“ markiert wurden. Standardmäßig aus, weil Abandonment ein deutlich weicheres Signal ist als ein veröffentlichter CVE. -
ignore
Eine Map oder Liste von Advisory-IDs, die unterdrückt werden sollen. Advisories können über ihre CVE-ID, ihre GHSA-ID oder Packagists PKSA-ID referenziert werden. Die Map-Form erlaubt es, eine Begründung mitzugeben, was in der Praxis wichtig ist, weil unterdrückte Advisories im Audit-Bericht weiterhin erscheinen und die nächste Person in der Wartung wissen sollte, warum du sie unterdrückt hast.
{
"config": {
"audit": {
"ignore": {
"CVE-2026-xxxxx": "Windows-only; we deploy Linux",
"GHSA-xxxx-xxxx-xxxx": "Patched inline via vendor override"
}
}
}
}
Es gibt zwei Hintertüren auf der Kommandozeile. Sowohl --no-security-blocking für einen einzelnen Aufruf als auch die Umgebungsvariable COMPOSER_NO_SECURITY_BLOCKING deaktivieren das Blockieren für diesen Lauf. Beide sind ausdrücklich verschieden von --no-audit und COMPOSER_NO_AUDIT, die nur den Audit-Bericht unterdrücken.
Filter-Listen
Security-Advisories sind nicht der einzige Grund, warum man möchte, dass der Resolver eine bestimmte Version zurückweist. Die jüngsten Wellen von Supply-Chain-Angriffen im JavaScript-Ökosystem haben zum Beispiel deutlich gemacht, dass kompromittierte oder schlicht bösartige Releases einen separaten Datenkanal brauchen. Ein CVE bedeutet eine bekannte Schwäche in Code, den die Autoren so schreiben wollten. Eine Malware-Markierung bedeutet, dass das Artefakt selbst eine Falle ist, die von einem Angreifer gestellt wurde, der an die Veröffentlichungs-Credentials gekommen ist. Beides sollte die Auflösung blockieren, aber sie kommen aus unterschiedlichen Feeds und haben unterschiedliche Vertrauensmodelle.
Composer 2.10 wird „Filter-Listen“ einführen, verfolgt in composer/composer#12786, um dies als allgemeine Abstraktion zu behandeln. Eine Filter-Liste ist eine benannte Menge von Einträgen, die jeweils aus einem Paketnamen, einem Versions-Constraint und einer optionalen Begründung sowie URL bestehen. Wenn das Filtern aktiviert ist (und das ist es standardmäßig), entfernt Composer jede Pool-Version, die auf einen Eintrag einer aktivierten Liste passt, und nutzt dafür dieselbe Pool-Filter-Stufe, die 2.9 für Advisories gebaut hat.
Die Terminologie, die das Issue einführt, trennt drei Aspekte:
- Sources sind Datenanbieter. Standardmäßig ist jedes konfigurierte Composer-Repository eine Source. Zusätzliche HTTP-Sources können in der
composer.jsonkonfiguriert werden. - Lists sind benannte Gruppen innerhalb einer Source. Ein Composer-Repository kann mehrere Lists anbieten (zum Beispiel
aikido-malwareneben anderen kuratierten Sets) und einige davon als Default markieren, sodass sie ohne weitere Konfiguration aktiviert sind, während andere Opt-in bleiben. - Entries sind die einzelnen Filter-Regeln innerhalb einer List.
Der erste produktive Einsatz auf Packagist.org ist die Integration mit dem Malware-Feed von Aikido, implementiert in composer/packagist#1681. Wenn eine Paket-Version auf Packagist als Malware markiert ist, bekommt die für dieses Paket ausgelieferte Metadaten-Datei einen filter-Block:
{
"filter": {
"aikido-malware": [{
"constraint": "1.0 || 1.1 || 1.2",
"url": "https://packagist.org/packages/vendor/name/filters",
"reason": "Package contains malicious code",
"id": "…"
}]
}
}
Die Top-Level-packages.json des Repositorys gibt an, welche Lists unterstützt werden und welche standardmäßig aktiv sind:
{
"filter": {
"metadata": true,
"lists": ["aikido-malware"],
"default-lists": ["aikido-malware"]
}
}
Dritte können eigene Sources anbinden. Eine unter filter.sources konfigurierte URL bekommt einen POST mit einer Liste von Paket-URLs im Format pkg://composer/vendor/name und gibt passende Filter-Einträge in derselben Form zurück. Damit kann eine Organisation einen internen Malware- oder Policy-Feed betreiben (zum Beispiel für Lizenzverletzungen, bekannte schlechte Forks oder Pakete in Quarantäne während eines Incidents), ohne ein vollwertiges Composer-Repository hosten zu müssen.
Filter-Listen werden mit "config": {"filter": false} in der composer.json deaktiviert, oder mit COMPOSER_FILTER=0. Derselbe pro-Eintrag-Ignorieren-Mechanismus, der für Advisories existiert, wird laut Design-Notiz auch auf Filter-Einträge ausgeweitet.
Architektonisch ist das dieselbe Pool-Filter-Stufe, die zwei Jobs erledigt: Advisories sind eine Datenart, die der Filter konsumiert, Malware-Markierungen eine andere, und zukünftige Kategorien fügen sich in dieselbe Pipeline ein.
Wo passt der alte Stack hinein?
FriendsOfPHP/security-advisories existiert weiterhin und ist weiterhin wichtig, aber jetzt als Upstream-Datenquelle und nicht mehr als direkter Eingang in dein Projekt. Packagist.org nimmt die Daten (zusammen mit denen von GitHub, Drupal SA und anderen) in die Advisory-API auf. Der Pool-Filter von Composer 2.9 liest diese API. Du bekommst dieselbe Abdeckung ohne eine Abhängigkeit in deiner composer.json. Mitwirkende bei FriendsOfPHP verbessern die Daten weiter, aber diese Arbeit erreicht Endnutzer jetzt über den Resolver und nicht mehr über eine neu gebaute Konflikt-Liste.
Roave/SecurityAdvisories wird ausdrücklich als überholt beschrieben. Es zusätzlich zum Blockieren von 2.9 installiert zu lassen, ist harmlos. Die beiden Mechanismen sind sich bei fast allen Versionen einig. Aber es fügt eine Abhängigkeit hinzu, verlangsamt die Auflösung ein wenig und bietet nichts, was das eingebaute Feature nicht bietet. Die Migration ist eine Entfernung in einer Zeile.
Das Einzige, was Roaves Paket noch besser kann, ist ein enger Randfall: das Blockieren zur composer require-Zeit mit einem veralteten Datensatz, auf einer Maschine, deren Composer-Version älter als 2.9 ist. Wenn du Composer nicht aktualisieren kannst, bleibt das alte Paket die beste Option. Überall sonst gilt die Empfehlung aus den Release Notes zu Composer 2.9: entferne es.
Ein Beispiel aus der Praxis
Eine nützliche Illustration für das Verhalten des Features, und für eine seiner scharfen Kanten, ist das, was heute im Zusammenhang mit einer Sicherheitslücke in PHPUnit 12.5.21 und PHPUnit 13.1.5, die ich gestern veröffentlicht habe, passiert ist. Heute Morgen erreichten mich per E-Mail, über Chatnachrichten und Telefonanrufe sowie hier und hier im Issue Tracker von PHPUnit Meldungen, dass PHPUnit 11 und frühere Versionen nicht mehr installiert werden könnten.
Die Abfolge, die dazu geführt hat, lohnt sich durchzugehen, denn sie zeigt, wie sich das Blockier-Feature mit normaler Constraint-Auflösung kombiniert:
- Ich veröffentliche PHPUnit 12.5.22 und PHPUnit 13.1.6 mit dem Sicherheitsfix.
- Ich veröffentliche Informationen über die Sicherheitslücke, die in PHPUnit 12.5.22 und PHPUnit 13.1.6 behoben ist, hier. Da nur PHPUnit 12.5.21 und PHPUnit 13.1.5 von dem Problem betroffen waren, trage ich
12.5.21und13.1.5als betroffene Versionen ein. - GitHub veröffentlicht das Security-Advisory offiziell hier. Dabei ändern sie die betroffenen Versionen zu
<= 12.5.21und>= 13.0.0, <= 13.1.5. - Packagist importiert das Security-Advisory aus der GitHub Advisory Database.
- Composer 2.9 beginnt, die Installation aller PHPUnit-Versionen mit einer Versionsnummer kleiner als
12.5.22zu blockieren, darunter zum Beispiel Versionen von PHPUnit 11, die von dem Problem gar nicht betroffen sind. - Ich spreche mit Nils Adermann und Jordi Boggiano, den Maintainern von Composer und Packagist, über diese Situation und erfahre, dass Daten aus
FriendsOfPHP/security-advisoriesVorrang vor Daten aus der GitHub Advisory Database haben. - Ich schicke einen Pull Request für
FriendsOfPHP/security-advisoriesmit den korrekten Informationen zu den betroffenen Versionen. Nils Adermann akzeptiert diesen Pull Request. - Packagist importiert das Security-Advisory von
FriendsOfPHP/security-advisories. - Zum Beispiel PHPUnit 11 lässt sich wieder mit Composer 2.9 installieren.
Auch wenn diese Situation mir einen Teil meines Wochenendes geraubt hat, zeigt sie, dass das mit Composer 2.9 eingeführte Automatic Security Blocking wie beabsichtigt funktioniert.
Ich hege keinen Groll gegen die, die das Problem an einem Samstag gemeldet haben, und ich hoffe, dass Nils und Jordi mir verzeihen, dass ich sie an einem Samstag um Hilfe gebeten habe. Was mich aber wirklich frustriert: GitHub hat die Informationen zu den „Affected versions“, die ich sorgfältig angegeben hatte, verändert, als sie mein Advisory veröffentlicht haben.
Es war natürlich genau diese Situation, die mich dazu gebracht hat, diesen Artikel an einem Samstag zu schreiben und zu veröffentlichen.
Praktische Hinweise
Für neue Projekte: nichts tun. Blockieren ist standardmäßig aktiv, die Defaults sind sinnvoll, und die Fehlermeldungen verweisen auf die Konfigurationsschlüssel, die du brauchst.
Für bestehende Projekte, die noch roave/security-advisories verwenden: nimm es beim nächsten Composer-Update aus require-dev heraus. Die nativen Features von Composer sind äquivalent oder besser.
Für Projekte, in denen ein Advisory ein Update blockiert, das du nicht sofort übernehmen kannst, weil der Fix in einer Major-Version liegt, auf die du noch nicht migriert bist, oder weil das Advisory deinen Anwendungsfall nicht trifft: verwende die ausführliche Form von ignore mit dem Scope apply: "all" und einer schriftlichen Begründung.
Für CI-Pipelines, die während eines Incidents temporär umgehen müssen: nutze COMPOSER_NO_SECURITY_BLOCKING=1, statt die composer.json zu verändern. Es ist im Build-Log sichtbar, es bleibt nicht bestehen, und es wird nicht versehentlich committet.
Für das Blockieren abgekündigter Pakete: überleg dir, es einzuschalten ("block-abandoned": true), sobald dein Projekt sauber ist. Es produziert mehr False Positives als das Blockieren per Advisory, aber es macht auch Supply-Chain-Risiken sichtbar, über die dir sonst nichts in deiner Toolchain etwas sagt.
Für Maintainer privater Composer-Repositories: Das Metadaten-Format für Advisories (ab 2.9) und für Filter-Listen (ab 2.10) ist dokumentiert und stabil. Interne Sicherheitsdaten darüber bereitzustellen, ist der richtige Integrationspfad, und genau das, was Private Packagist für seine Kunden tut.
Fazit
Was Composer 2.9 getan hat, ist auf dem Papier schmal: eine einzelne Filterstufe in den Resolver für Abhängigkeiten einzufügen. Aber in der Praxis verschiebt es einen Baustein der Supply-Chain-Sicherheit aus einem optionalen Drittanbieter-Paket in den Standard-Installationspfad jedes PHP-Projekts. Composer 2.10 wird denselben Mechanismus verallgemeinern, sodass Malware-Markierungen, und was auch immer als Nächstes kommt, sich in ihn einklinken können, ohne das Gerüst neu erfinden zu müssen.
Die alten Datenquellen sind nicht verschwunden. Das YAML von FriendsOfPHP ist immer noch das Rohmaterial hinter einem großen Teil der Advisories, die das neue System durchsetzt; es erreicht Endnutzer jetzt einfach über einen anderen Ausliefermechanismus. Roaves Paket hat seinen Job ein Jahrzehnt lang gemacht, und ein Teil dieses Jobs war, zu beweisen, dass das Ökosystem dieses Verhalten standardmäßig haben will. Was es jetzt endlich hat.