131% mehr gemeldete CVEs pro Tag in 2026 als im Vorjahr.

PHP-Sicherheit

PHP-Sicherheit: PHP-Schwachstellen in Ihrem Code, Ihren Composer-Abhängigkeiten und Ihrem Build verhindern

PHP abzusichern heißt, drei Ebenen abzusichern. Im Code, den Sie schreiben: Eingaben validieren, parametrisierte Abfragen und das Escaping des Frameworks nutzen und Daten aus nicht vertrauenswürdigen Quellen nie deserialisieren. In den Composer-Paketen, von denen Sie abhängen: composer audit ausführen, composer.lock committen und unsichere Versionen blockieren. Im CI, das sie installiert: aufzeichnen, was tatsächlich installiert wurde, und einschränken, wohin sich der Build verbinden darf. Die meisten Leitfäden zur PHP-Sicherheit hören nach der ersten Ebene auf. Dieser deckt alle drei ab.

Aktualisiert am

  1. Ihr Code Injection, XSS, Deserialisierung, Dateiverarbeitung, Sessions Framework-Standards, Code-Review, PHPStan, Psalm, Semgrep
  2. Ihre Composer-Abhängigkeiten Bekannte Schwachstellen, aufgegebene und bösartige Pakete, Lockfile-Drift, Code bei der Installation composer audit, Composer-Policies, Dependabot oder Renovate
  3. Ihr Build Was das CI tatsächlich installiert und ausgeführt hat, woher es geladen hat, was es erreichen konnte Gepinnte Actions, Tokens mit minimalen Rechten, Egress-Policy, SBOM aus dem Build
Die drei Ebenen der PHP-Sicherheit und die Tools für jede Ebene

Auf dieser Seite: PHP2Shell, die PHP-Schwachstellen, die noch zählen, Konfiguration und unterstützte Versionen, statische Analyse, Sicherheit von Composer-Abhängigkeiten, Supply-Chain-Angriffe auf PHP, der CI-Build, SBOMs und Tools nach Ebene.

PHP2Shell: die PHP-Schwachstelle, die Sie jetzt patchen sollten

Am 21. Oktober 2026 hat das PHP-Projekt PHP 8.5.12, 8.4.27, 8.3.36 und 8.2.35 veröffentlicht. Die Releases beheben zwei kritische Schwachstellen in PHP selbst, die eine Remote-Codeausführung ohne Authentifizierung ermöglichen können. Beide sind nach CVSS v4.0 mit 9,2 bewertet und werden zusammen PHP2Shell genannt:

  • CVE-2026-109618: Die Behandlung von Speichermangel kann PHP unterbrechen, während Request-Daten erst teilweise initialisiert sind, und die Bereinigung des Requests behandelt die ungültigen Daten dann womöglich als gültig.
  • CVE-2026-109619: Zustand, der zu einem Request gehört, kann die Freigabe seines Speichers überdauern, und der nächste Request, den derselbe Worker bearbeitet, kann ihn verwenden.

Laut dem Advisory von PHP ist jede Installation gefährdet, die Web-Requests bedient, darunter PHP-FPM, Apache mit mod_php, php-cgi, FrankenPHP und LiteSpeed; das höchste Risiko sieht es bei mod_php. PHP 8.1 und älter erhalten vom PHP-Projekt kein neues Release. Wegen des Schweregrads hat das PHP-Sicherheitsteam jedoch minimale Source-Backports für PHP 7.4, 8.0 und 8.1 veröffentlicht, für alle, die PHP aus dem Quellcode bauen oder keinen LTS-Anbieter haben; einige Distributionen und LTS-Anbieter liefern zudem eigene zurückportierte Fixes. Was zu tun ist:

  • Aktualisieren Sie auf ein korrigiertes Release. Unter 8.1 oder älter wechseln Sie auf einen unterstützten Zweig, nehmen das zurückportierte Paket Ihres Anbieters oder wenden den offiziellen Source-Backport an, wenn Sie PHP selbst bauen.
  • Bauen Sie jedes Container-Image, das PHP enthält, neu und deployen Sie es erneut. Ein altes Image-Tag führt das alte PHP aus, bis es ersetzt wird. Prüfen Sie also, was deployt ist, nicht nur, was im Repository steht: docker run --rm IMAGE php -v.
  • Wenn Sie heute nicht aktualisieren können, wenden Sie die vorläufigen Gegenmaßnahmen aus dem PHP-Sicherheits-Advisory an, etwa session.upload_progress.enabled=0. Setzen Sie es in der php.ini, im FPM-Pool oder in einer Serverkonfiguration, die vor dem Parsen des Requests greift, und starten Sie jeden Worker neu. Verlassen Sie sich dafür nicht auf ini_set() oder die .user.ini von FPM: Das Advisory rät von beidem ab. PHP stellt klar, dass diese Maßnahmen die Angriffsfläche verringern, eine Installation aber nicht sicher machen.

Beachten Sie, was Ihnen composer audit hier nicht sagen kann: PHP2Shell steckt in der Runtime, nicht in einem Composer-Paket, und taucht daher nie in der composer.lock auf. Das PHP, das Sie ausführen, stammt aus Ihrem Basis-Image oder Ihren Betriebssystempaketen, also aus der dritten Ebene dieses Leitfadens. Die Versionstabelle, die Zeitleiste und ein Skript, das jedes Image-Tag prüft, finden Sie unter PHP2Shell: was Sie patchen müssen und wie Sie jedes verwundbare PHP finden.

Die PHP-Schwachstellen, die noch zählen

Die Schwachstellenklassen in PHP-Anwendungen haben sich in einem Jahrzehnt kaum verändert. Verändert hat sich, dass Laravel, Symfony und modernes PHP die meisten davon standardmäßig schließen. Echte Fehler häufen sich daher dort, wo Code diese Standards umgeht. Injection steht in den OWASP Top 10:2025 auf A05, und Software Supply Chain Failures ist neu auf A03. Darum geht es in der zweiten Hälfte dieses Leitfadens.

Schwachstelle Wie sie in PHP auftritt Die Lösung Laravel und Symfony
SQL-Injection Request-Daten werden in SQL verkettet, auch über Hintertüren für rohe Abfragen PDO-Prepared-Statements mit gebundenen Parametern Eloquent und Doctrine binden Parameter; whereRaw(), DB::raw() und verkettetes DQL nicht
Cross-Site-Scripting (XSS) Benutzereingaben werden ohne Escaping in HTML ausgegeben Bei der Ausgabe escapen, mit htmlspecialchars() oder der Template-Engine Blade {{ }} und Twig escapen standardmäßig; {!! !!} und |raw nicht
Cross-Site Request Forgery (CSRF) Ein Formular oder AJAX-Endpunkt, der Zustand ändert, akzeptiert jeden Request, der das Session-Cookie mitschickt Ein Token pro Session, geprüft bei jedem POST, PUT, PATCH und DELETE, dazu SameSite-Cookies Laravel prüft das Token auf web-Routen (@csrf in Formularen); Symfony Forms enthalten und prüfen standardmäßig eines; von der Prüfung ausgenommene Routen haben keinen Schutz
Code Injection über eval() Request-Daten oder ein Template bzw. eine Formel, die ein Nutzer bearbeiten kann, gelangen in eval() Kein eval() verwenden; Eingaben auf eine feste Menge von Operationen abbilden oder einen Parser für die benötigten Ausdrücke nutzen Kein Framework schützt Ihre eigenen eval()-Aufrufe
Object Injection unserialize() auf Cookies, Request-Daten oder Cache-Einträgen, die ein Angreifer schreiben kann JSON für Daten von außen verwenden; wenn Sie deserialisieren müssen, Objekte ablehnen Kein Framework schützt Ihre eigenen unserialize()-Aufrufe
File Inclusion und Uploads Ein aus Eingaben gebauter Pfad wird an include übergeben; Uploads liegen unter dem Web-Root mit dem Dateinamen des Clients Eingebundene Dateien per Allowlist festlegen; Uploads außerhalb des Web-Roots unter generierten Namen speichern Laravels store() generiert Dateinamen; Typ und Größe auf dem Server validieren
Type Juggling == vergleicht Tokens oder Hashes, wobei "0e123" == "0e456" wahr ist ===, hash_equals() für Geheimnisse, password_verify() für Passwörter Framework-Helfer vergleichen sicher; Ihre eigenen Vergleiche liegen bei Ihnen
Schwache Sessions und Cookies Session-IDs, die JavaScript lesen kann, die über HTTP gesendet oder über den Login hinweg beibehalten werden Secure-, HttpOnly- und SameSite-Cookies, Strict Mode, eine neue Session-ID nach dem Login Beide erneuern die Session-ID beim Login
Server-Side Request Forgery (SSRF) file_get_contents() oder cURL auf eine vom Benutzer gelieferte URL, auch mit Wrappern wie file:// und phar:// Schemata und Hosts per Allowlist festlegen, interne Adressen sperren, allow_url_fopen abschalten, wo es nicht gebraucht wird Kein Schutz standardmäßig
Command Injection Eingaben werden an exec(), shell_exec(), system() oder proc_open() übergeben Die Shell vermeiden; Argumente als Array übergeben oder jedes mit escapeshellarg() escapen Symfony Process und Laravels Process-Facade akzeptieren ein Argument-Array
Mass Assignment User::create($request->all()) lässt einen Request is_admin setzen Befüllbare Felder auflisten oder nur validierte Eingaben übergeben Eloquent-Modelle sind standardmäßig geschützt; $guarded = [] schaltet das ab

SQL-Injection: verwundbar und behoben

// Vulnerable: the id goes straight into the SQL string
$rows = $pdo->query("SELECT * FROM orders WHERE id = " . $_GET['id']);

// Fixed: a prepared statement with a bound parameter
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?');
$stmt->execute([$_GET['id']]);
$rows = $stmt->fetchAll();

// Laravel: whereRaw() takes SQL as written, where() binds the value
Order::whereRaw("status = '" . $request->input('status') . "'")->get(); // vulnerable
Order::where('status', $request->input('status'))->get();             // fixed

XSS: verwundbar und behoben

<?php // Plain PHP: escape on output
echo '<p>' . $comment . '</p>';                                              // vulnerable
echo '<p>' . htmlspecialchars($comment, ENT_QUOTES, 'UTF-8') . '</p>';    // fixed ?>

{{-- Blade: {{ }} escapes, {!! !!} prints raw HTML --}}
<p>{!! $comment !!}</p>   {{-- vulnerable --}}
<p>{{ $comment }}</p>     {{-- fixed --}}

{# Twig: autoescape is on, |raw turns it off #}
<p>{{ comment|raw }}</p>  {# vulnerable #}
<p>{{ comment }}</p>      {# fixed #}

Object Injection: verwundbar und behoben

// Vulnerable: an attacker-controlled cookie becomes PHP objects, and any
// class with a __wakeup() or __destruct() method in vendor/ is a gadget
$prefs = unserialize($_COOKIE['prefs']);

// Fixed: JSON carries data, not objects
$prefs = json_decode($_COOKIE['prefs'], true, 512, JSON_THROW_ON_ERROR);

// If you must read serialized data, refuse objects
$prefs = unserialize($data, ['allowed_classes' => false]);

Phar-Deserialisierung, die Variante, bei der eine Dateioperation auf einem phar://-Pfad die Metadaten des Archivs deserialisierte, wurde in PHP 8.0 eingeschränkt: Phar-Metadaten werden beim Öffnen eines Phar nicht mehr deserialisiert, sondern nur noch, wenn Code getMetadata() aufruft. Ein Grund mehr, PHP 7 hinter sich zu lassen.

File Inclusion: verwundbar und behoben

// Vulnerable: ?page=../admin/reset reaches any .php file on the disk
include __DIR__ . '/pages/' . $_GET['page'] . '.php';

// Fixed: only names on an allow-list can be included
$pages = ['home' => 'home.php', 'pricing' => 'pricing.php'];
$file = $pages[$_GET['page'] ?? 'home'] ?? 'home.php';
include __DIR__ . '/pages/' . $file;

Härtung von Konfiguration und Runtime

Auch eine sichere Anwendung kann über ihre Runtime Daten preisgeben. Diese php.ini-Einstellungen folgen dem OWASP PHP Configuration Cheat Sheet:

Einstellung Wert in Produktion Warum
display_errors Off (mit log_errors = On) Stack Traces zeigen Pfade, Abfragen und manchmal Geheimnisse
expose_php Off Verhindert, dass die genaue PHP-Version in Response-Headern angezeigt wird
allow_url_include Off Verhindert include entfernter URLs; seit PHP 7.4 veraltet
allow_url_fopen Off, wenn die Anwendung keine URLs mit Dateifunktionen abruft Engt SSRF über file_get_contents() ein
disable_functions Zum Beispiel exec, shell_exec, system, passthru, proc_open, popen Nimmt einem Angreifer, der Code ausführen kann, die Shell; listen Sie nur auf, was Ihre Anwendung nie aufruft
open_basedir Das Anwendungsverzeichnis Begrenzt die Pfade, die PHP-Dateifunktionen öffnen können; eine Leitplanke, keine Isolationsgrenze
Session-Cookies session.cookie_secure = 1, session.cookie_httponly = 1, session.cookie_samesite = Lax oder Strict, session.use_strict_mode = 1 Hält Session-IDs von unverschlüsseltem HTTP und von JavaScript fern und lehnt IDs ab, die der Server nicht ausgegeben hat
file_uploads Off, wenn die Anwendung keine Uploads annimmt Entfernt einen ganzen Eingabepfad

Bleiben Sie auf einer unterstützten PHP-Version

PHP gibt jedem Zweig zwei Jahre aktiven Support und danach zwei Jahre nur Sicherheitsfixes, so php.net. PHP 8.1 hat am 31. Dezember 2025 das Ende seiner Lebensdauer erreicht, PHP 8.2 folgt am 31. Dezember 2026. Ein Zweig nach dem End of Life erhält vom PHP-Projekt keine neuen Releases mehr. Genau in diese Lage hat PHP2Shell die Nutzer von 8.1 gebracht: Die einzige offizielle Korrektur für sie war ein Source-Backport.

  • PHP 8.1 End of Life, endete am 31. Dez. 2025
  • PHP 8.2 Nur Sicherheitsfixes, bis 31. Dez. 2026
  • PHP 8.3 Nur Sicherheitsfixes, bis 31. Dez. 2027
  • PHP 8.4 Aktiver Support, bis 31. Dez. 2026
  • PHP 8.5 Aktiver Support, bis 31. Dez. 2027
Unterstützte PHP-Versionen (Daten von php.net)

Produktionseinstellungen für Laravel und Symfony

  • Debug aus. APP_DEBUG=false in Laravel, APP_ENV=prod und APP_DEBUG=0 in Symfony. Eine Debug-Seite in Produktion zeigt Konfigurations- und Umgebungswerte.
  • Schlüssel und Geheimnisse. Halten Sie APP_KEY (Laravel) und APP_SECRET (Symfony) aus dem Repository heraus und rotieren Sie sie, wenn sie geleakt werden. Mit Laravels APP_PREVIOUS_KEYS rotieren Sie den Schlüssel, ohne alle Benutzer abzumelden.
  • Vertrauenswürdige Proxys. Hinter einem Load Balancer listen Sie die Proxys auf, denen Sie vertrauen (die Trusted-Proxies-Middleware von Laravel, framework.trusted_proxies in Symfony), damit ein Client seine IP-Adresse oder HTTPS nicht mit X-Forwarded-*-Headern fälschen kann.

Statische Analyse für PHP (SAST)

Statische Analyse liest Ihren Code, ohne ihn auszuführen. Drei Open-Source-Tools decken die meisten PHP-Teams ab:

  • PHPStan findet Typfehler und Bugs auf zunehmend strengen Stufen. Es ist kein Sicherheitsscanner, aber viele Sicherheitsfehler beginnen als Typverwechslung oder als ungeprüftes null.
  • Psalm ergänzt eine Taint-Analyse: Es verfolgt Daten aus Quellen wie $_GET und $_COOKIE bis zu Senken wie echo, include und SQL-Abfragen und meldet Probleme wie TaintedSql und TaintedHtml. Aktuelle Psalm-Versionen führen sie zusammen mit der Typprüfung aus; --taint-analysis schaltet sie für einen einzelnen Lauf ein, und --report=results.sarif schreibt SARIF für GitHub Code Scanning.
  • Semgrep unterstützt PHP als allgemein verfügbare Sprache, mit Community-Regeln für häufige PHP-Fehler und funktionsübergreifendem Datenfluss in der kommerziellen Stufe.

SAST ist bei Legacy-PHP aus strukturellen Gründen laut: dynamische Includes, variable Variablen, magische Methoden wie __get() und __call() sowie Framework-Konventionen, die Daten durch Container und Facades leiten, denen der Analyzer nicht folgen kann. Das Tool rät entweder und meldet False Positives, oder es gibt auf und übersieht echte Datenflüsse.

Auf einer alten Codebasis führen Sie es am besten als Ratsche ein. Halten Sie die heutigen Befunde als Baseline fest (vendor/bin/phpstan analyse --generate-baseline schreibt phpstan-baseline.neon; Psalm hat --set-baseline), lassen Sie den Build nur bei neuen Befunden fehlschlagen und erhöhen Sie die Stufe, während Sie die Baseline abbauen. Teams mit langlebigen PHP-Codebasen berichten uns, dass KI-Autofix bei Abhängigkeits-Updates gut funktioniert und bei Code-Befunden durchwachsen ist. Prüfen Sie generierte Fixes für SAST-Befunde also so sorgfältig wie jede andere Änderung. SAST und SCA beantworten unterschiedliche Fragen; siehe SAST vs. SCA.

Sicherheit von Composer-Abhängigkeiten

Eine Laravel- oder Symfony-Anwendung installiert Dutzende Composer-Pakete, die meisten davon transitiv. Composer hat in den letzten Releases echte Sicherheitsfunktionen bekommen; dieser Abschnitt bezieht sich auf Composer 2.11, das aktuelle Release.

composer audit

composer audit gleicht die installierten Pakete mit Sicherheits-Advisories aus der Packagist.org-API ab, die Advisories aus Quellen wie der GitHub Advisory Database und FriendsOfPHP/security-advisories sammelt. Es meldet außerdem aufgegebene Pakete und, seit Composer 2.10, als Malware markierte Paketversionen. Alle Optionen finden Sie in der Composer-Dokumentation.

composer audit                      # audit what is installed in vendor/
composer audit --locked             # audit composer.lock, whatever is in vendor/
composer audit --no-dev             # skip require-dev packages
composer audit --direct             # direct dependencies only (Composer 2.11)
composer audit --format=json        # table, plain, json or summary
composer audit --abandoned=report   # ignore, report or fail on abandoned packages
composer audit --ignore-severity=low

Seit Composer 2.10 ist der Exit-Code 0, wenn es keine Probleme gibt, und 1, wenn etwas das Audit nicht bestanden hat. Damit funktioniert es ohne Parsen als CI-Gate. Aufgegebene Pakete lassen das Audit standardmäßig fehlschlagen.

Akzeptierte Befunde gehören mit Begründung in die composer.json, nicht in ein CI-Flag, an das sich niemand erinnert. Composer 2.10 hat den Großteil der alten audit-Konfiguration durch einen policy-Block ersetzt:

{
    "config": {
        "policy": {
            "advisories": {
                "ignore-id": {
                    "CVE-1234": "Not affected: we never call the vulnerable parser."
                }
            },
            "abandoned": {
                "audit": "report"
            }
        }
    }
}

Die alten Schlüssel (audit.ignore, audit.abandoned, audit.block-insecure) funktionieren weiter als Fallback, solange der passende policy-Abschnitt fehlt, sind aber veraltet. Migrieren Sie alle Advisory-Einstellungen auf einmal: Sobald policy.advisories existiert, wird jeder Advisory-bezogene audit.*-Schlüssel ignoriert.

Unsichere Versionen blockieren

Seit Composer 2.9 (November 2025) lehnen composer update und composer require Paketversionen ab, für die ein Sicherheits-Advisory existiert. Das war die Einstellung audit.block-insecure und ist jetzt policy.advisories.block, standardmäßig aktiv. Composer 2.10 hat das Blockieren von Malware aus dem Malware-Feed von Packagist ergänzt, das auch für composer install gilt. Composer 2.11 hat eine optionale cooldown-Policy ergänzt, die neu veröffentlichte Versionen für einen Zeitraum Ihrer Wahl zurückhält, etwa "7 days", denn viele bösartige Versionen werden innerhalb von Stunden oder Tagen entdeckt und entfernt.

Im CI zählen zwei Details. Erstens greift das Blockieren von Advisories, wenn Versionen aufgelöst werden: Eine Version, die bereits in der composer.lock steht, wird mit composer install auch nach Veröffentlichung eines Advisorys installiert. Sie brauchen also weiterhin composer audit als Gate. Zweitens schaltet --no-blocking für einen Befehl jede Policy ab. Danach lohnt es sich, Ihre Pipelines zu durchsuchen.

Vor Composer 2.9 blockierte man unsichere Versionen meist mit roave/security-advisories, einem Paket, das einen conflict mit jeder bekannten verwundbaren Version deklariert und aus der GitHub Advisory Database und FriendsOfPHP erstellt wird. Es funktioniert weiterhin und hilft bei älteren Composer-Versionen; ab Composer 2.9 deckt das integrierte Blockieren dieselben Advisories ohne das zusätzliche Paket ab.

Disziplin beim Lockfile

  • Committen Sie die composer.lock für Anwendungen, damit jede Umgebung dieselben Versionen installiert.
  • Nutzen Sie composer install in CI und Deployments, nie composer update, das spontan neue Versionen auflöst.
  • Installieren Sie Produktions-Builds mit --no-dev, damit Test-Tools nie ausgeliefert werden.
  • Führen Sie composer validate --strict aus: Es schlägt fehl, wenn die composer.lock nicht zur composer.json passt.

Ein Lockfile hält fest, was Composer aufgelöst hat. Siehe was ein Lockfile nicht aufzeichnet.

Code bei der Installation: Plugins, Skripte und autogeladene Dateien

Composer führt Code aus Ihren Abhängigkeiten an mehr Stellen aus, als die meisten Teams erwarten. Damit ist es das PHP-Gegenstück zu npm-Install-Skripten:

  • Plugins laufen innerhalb von Composer selbst. Seit Composer 2.2 muss die Einstellung allow-plugins jedes Plugin auflisten, das laufen darf, und standardmäßig erlaubt sie keines. Behandeln Sie einen neuen Eintrag dort wie eine neue Abhängigkeit mit Shell-Zugriff.
  • Skripte laufen nur aus Ihrer Root-composer.json, nicht aus Abhängigkeiten. Sie starten aber oft Ihre Anwendung.
  • Autogeladene Dateien laufen, sobald der Autoloader geladen wird. In einer Standard-Laravel-Anwendung endet composer install mit php artisan package:discover, das den Autoloader lädt. Die autogeladenen files einer Abhängigkeit laufen also während der Installation in Ihrem CI-Job.

Braucht ein Job keine Plugins oder Skripte, verhindert --no-plugins --no-scripts, dass Code aus Abhängigkeiten während der Installation läuft; führen Sie die nötigen Schritte danach explizit aus.

Repository-Quellen und Dependency Confusion

Composer kann von Packagist.org installieren, aus privaten Composer-Repositories wie Private Packagist, aus VCS-Repositories (Git-URLs) und aus lokalen Pfaden. Wo private und öffentliche Quellen gemischt werden, entsteht Dependency Confusion: Wenn der Name Ihres privaten Pakets auch auf Packagist.org mit einer höheren Version existiert, welches bekommen Sie? In Composer 2 ist jedes Repository kanonisch, standardmäßig: Hat Composer ein Paket in einem Repository gefunden, sucht es weiter unten in der Liste nicht mehr danach, und Packagist.org steht immer zuletzt. Wer "canonical": false für ein Repository setzt, entfernt diesen Schutz, also tun Sie es nur bewusst. VCS-Repositories liefern, was der Git-Host für ein Tag ausgibt. Pinnen Sie sie daher im Lockfile auf einen Commit und prüfen Sie Änderungen daran.

Aufgegebene und nicht gewartete Pakete

Legacy-PHP-Landschaften enthalten Pakete, deren Maintainer weitergezogen sind. Packagist markiert ein Paket als aufgegeben, wenn sein Maintainer das erklärt, oft mit einem Ersatz, und composer audit schlägt dann standardmäßig fehl. Viele nicht gewartete Pakete werden allerdings nie formal aufgegeben. Prüfen Sie das letzte Release-Datum bei allem, was Eingaben, Authentifizierung oder Kryptografie verarbeitet, und planen Sie Ersatz, bevor eine Schwachstelle Sie dazu zwingt.

Sicher aktualisieren, wenn es kaum Tests gibt

Der schwierigste Fall ist eine große Legacy-Anwendung mit einer verwundbaren Abhängigkeit und wenigen Tests. Was funktioniert:

  1. Aktualisieren Sie das Paket, das das verwundbare nachzieht. Die meisten Befunde sind transitiv; composer why vendor/package zeigt die Kette, und Sie aktualisieren zuerst das übergeordnete Paket innerhalb seiner Constraint.
  2. Aktualisieren Sie eins nach dem anderen. composer update vendor/package --with-dependencies hält die Änderung klein genug, um sie im Diff der composer.lock zu prüfen.
  3. Testen Sie zuerst die kritischen Pfade per Smoke-Test. Login, Checkout, die wichtigsten API-Endpunkte: Eine Handvoll End-to-End-Prüfungen fängt die meisten Fehler durch Upgrades ab.
  4. Ergänzen Sie Charakterisierungstests rund um das, was das Upgrade berührt. Halten Sie fest, was der Code heute tut, damit Sie merken, wenn das Upgrade es ändert.
  5. Rollen Sie stufenweise aus und beobachten Sie die Fehlerraten, bevor es jeden Kunden erreicht.

Was composer audit übersieht

Das Lockfile ist ein Plan, keine Aufzeichnung des Builds. composer audit liest vendor/ oder die composer.lock und sieht daher nicht:

  • PHP selbst und seine Extensions, die aus Ihrem Basis-Image oder Betriebssystempaketen stammen (PHP2Shell ist ein Beispiel).
  • PECL-Extensions und Systembibliotheken, die im Docker-Image installiert sind.
  • Tools, die Ihr CI mit curl lädt, Actions, die es ausführt, und Binärdateien, die ein Plugin oder Skript herunterlädt.
  • Bösartigen Code, für den es noch kein Advisory und keine Malware-Markierung gibt.

Dieselben Grenzen gelten für npm audit und pip-audit in ihren Ökosystemen.

Supply-Chain-Angriffe auf PHP

Vier Vorfälle zeigen, wo Supply-Chain-Angriffe auf PHP tatsächlich ansetzen und was jeden davon erkannt hätte.

März 2021: bösartige Commits in php-src

Am 28. März 2021 wurden unter den Namen zweier Core-Entwickler zwei bösartige Commits in das php-src-Repository gepusht. Das PHP-Team kündigte an, seinen eigenen Git-Server nicht mehr zu betreiben und die GitHub-Repositories kanonisch zu machen. Seine Folgeuntersuchung ergab, dass die Commits über HTTPS mit Passwort-Authentifizierung gepusht worden waren, möglicherweise mit Zugangsdaten aus einer geleakten Benutzerdatenbank, und nicht über einen kompromittierten Server. Was es erkannt hätte: nichts in Ihrer eigenen Pipeline. Der Angriff zielte auf die Sprache selbst, eine Erinnerung daran, dass auch die PHP-Runtime eine Abhängigkeit ist.

April 2021: Command Injection in Composer (CVE-2021-29472)

Ein Fehler bei der Verarbeitung von Mercurial-Repository-URLs in Composer ermöglichte Command Injection. Packagist berichtete, dass Packagist.org und Private Packagist über präparierte Paket-URLs theoretisch angreifbar waren, ging nicht von einer Ausnutzung aus und behob den Fehler in Composer 2.0.13 und 1.10.22. Was es erkannt hätte: Composer selbst auf Entwicklerrechnern und in CI-Images aktuell zu halten. Der Paketmanager ist Teil Ihrer Lieferkette.

Mai 2023: Übernahme von Packagist-Konten

Am 1. Mai 2023 meldete sich ein Angreifer mit anderswo geleakten Passwörtern bei vier inaktiven Packagist.org-Konten an, die zusammen 14 Pakete kontrollierten, und richtete die Pakete auf geforkte Repositories aus. Die Forks enthielten eine geänderte Beschreibung und keinen bösartigen Code. Packagist stellte sie am nächsten Morgen wieder her. Was es erkannt hätte: eine committete composer.lock, denn composer install nutzt die im Lockfile festgehaltenen Quellen, und die Prüfung von Lockfile-Diffs auf unerwartete URLs, die Packagist selbst empfiehlt. Für Maintainer: eindeutige Passwörter und Zwei-Faktor-Authentifizierung.

2026: gekaperte Tags bei laravel-lang und intercom-php

2026 veröffentlichten Angreifer mit gestohlenen Zugängen bösartige Releases bestehender Pakete. Packagist nennt intercom/intercom-php (30. April) und die laravel-lang-Pakete (22. Mai) in seinem Update zur Supply-Chain-Sicherheit. Im Fall laravel-lang zeigten Versions-Tags auf Commits in einem bösartigen Fork, und die Payload, eine src/helpers.php-Datei, die vom Autoloader von Composer geladen wurde, holte einen Credential Stealer für Cloud-Schlüssel, SSH-Schlüssel und .env-Dateien. Aikido zählte 233 kompromittierte Versionen. Was es erkannt hätte: Sobald sie markiert sind, blockiert die Malware-Policy von Composer 2.10 diese Versionen auch bei composer install; davor hält eine Cooldown-Phase brandneue Versionen fern, eine Egress-Policy hindert die Payload daran, ihren Server zu erreichen, und eine Aufzeichnung jedes Builds zeigt, welche Builds eine schlechte Version installiert haben.

Seitdem hat Packagist angekündigt, dass es neu getaggte stabile Versionen ablehnt, ein Transparenzlog für Eigentümer- und Tag-Änderungen veröffentlicht und seit März 2026 Malware auf Packagist.org markiert.

Den PHP-Build im CI absichern

Im Build treffen Ihr Code, Ihre Abhängigkeiten und Ihre Geheimnisse aufeinander. Dort richtet ein bösartiges Paket den größten Schaden an. Ein GitHub-Actions-Workflow für einen Produktions-Build:

name: build
on: [push, pull_request]

permissions:
  contents: read            # least privilege for GITHUB_TOKEN

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0
        with:
          php-version: "8.4"
          tools: composer:v2
          coverage: none

      - run: composer validate --strict

      - id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
      - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: composer-${{ hashFiles('composer.lock') }}

      - run: composer install --no-dev --no-interaction --no-progress --optimize-autoloader
      - run: composer audit --no-dev
  • Token mit minimalen Rechten. permissions: contents: read hindert einen kompromittierten Schritt daran, mit GITHUB_TOKEN Code oder Releases zu pushen. Siehe Berechtigungen des GitHub-Tokens.
  • Gepinnte Actions. Ein vollständiger Commit-SHA lässt sich nicht verschieben wie ein Tag; der Kommentar hält die Version für Dependabot oder Renovate lesbar.
  • Downloads cachen, nicht vendor/. Wenn Sie den Download-Cache von Composer cachen, läuft composer install in jedem Job gegen das Lockfile. Einem wiederhergestellten vendor/ wird so vertraut, wie es ist, und Caches lassen sich über Workflows hinweg vergiften; siehe GitHub-Actions-Cache.
  • Aufzeichnen, was installiert wurde. Das Lockfile sagt, was installiert werden sollte. Eine im Build aufgezeichnete SBOM sagt, was installiert wurde, einschließlich der PHP-Runtime, der Betriebssystempakete und der Downloads außerhalb von Composer. Siehe SBOMs aus dem Build.
  • Einschränken, wohin sich der Build verbinden darf. Ein Composer-Build braucht Packagist, GitHub und Ihr privates Repository. Eine Egress-Policy, die nur diese erlaubt, hindert einen Credential Stealer daran, seine Beute zu versenden.

Eine SBOM für ein PHP-Projekt erstellen

Die übliche Open-Source-Option ist das CycloneDX-Composer-Plugin. Als Plugin braucht es einen allow-plugins-Eintrag:

composer require --dev cyclonedx/cyclonedx-php-composer
composer CycloneDX:make-sbom --output-format=JSON --output-file=sbom.json

Es liest die composer.json zusammen mit den installierten Paketen oder der composer.lock und listet daher Ihre Composer-Abhängigkeiten genau auf, aber auch nichts anderes. Die PHP-Runtime, ihre Extensions, die apt- oder apk-Pakete des Basis-Images und die Tools, die der Job heruntergeladen hat, fehlen darin. Eine SBOM aus dem Build hält auch diese fest, weil sie aus dem entsteht, was der Job geladen hat, und nicht aus den Dateien von Composer. Zu den Formaten und dazu, was eine SBOM enthalten sollte, siehe was ist eine SBOM.

PHP-Sicherheitstools nach Ebene

Ebene Tools Was sie abdecken Was sie übersehen
Code PHPStan, Psalm, Semgrep Bugs und Typfehler, kontaminierte Datenflüsse, bekannte unsichere Muster Verwundbare Abhängigkeiten, die Runtime, den Build
Abhängigkeiten composer audit, Composer-Policies, roave/security-advisories, Dependabot oder Renovate Veröffentlichte Advisories, aufgegebene und markierte Pakete, Update-Pull-Requests Ihren eigenen Code und alles außerhalb der composer.lock
Image Container-Scanner Betriebssystem- und Sprachpakete in einem gebauten Image Was der Build geladen und nicht behalten hat, und Images, die niemand gescannt hat
SBOM CycloneDX-Composer-Plugin Composer-Pakete als CycloneDX-SBOM Alles außerhalb von Composer
Build CRACI Was jeder GitHub-Actions-Job tatsächlich installiert hat, aus Composer und jeder anderen Quelle, über die Zeit überwacht Ihren PHP-Code (kein SAST); andere CI-Systeme als GitHub Actions

Mehr zu Images: Container-Scanning. Weniger, bessere Befunde über alle Ebenen hinweg: so reduzieren Sie das Rauschen bei Schwachstellen.

PHP-Sicherheit und CRACI

CRACI ist ein GitHub-Actions-Runner, der aufzeichnet, was jeder Job tatsächlich installiert. Ändern Sie runs-on in craci, und jedes Composer-Paket, das der Job herunterlädt, wird mit seiner Package-URL (pkg:composer/vendor/package) aufgezeichnet, zusammen mit Paketen aus dem Betriebssystem-Paketmanager, Container-Images, Git-Quellen und anderen Downloads im selben Job, einschließlich der aus CI-Caches wiederhergestellten Pakete. Jede Komponente in der SBOM war im Build.

CRACI gleicht diese Komponenten mit bekannten Schwachstellen ab und prüft die SBOMs des Ausgelieferten laufend erneut, sobald neue Advisories veröffentlicht werden. So führt Sie auch eine CVE, die erst nächstes Jahr bekannt wird, zu den Builds, die sie enthalten. Policy Gates können einen Build bei Befunden stoppen, auch einen Build, der eine bestimmte CVE enthält, und die Egress-Policy begrenzt, wohin sich der Job verbinden kann.

CRACI scannt Ihren PHP-Code nicht: Es gibt bewusst kein SAST. Behalten Sie PHPStan, Psalm oder Semgrep für die Code-Ebene. Sehen Sie jedes Paket, das Ihr PHP-Build installiert.

Wann CRACI nicht die Antwort ist

  • Sie deployen per FTP oder auf Shared Hosting ohne CI. Es gibt keinen Build, den man aufzeichnen könnte. Beginnen Sie mit composer audit und einer committeten composer.lock.
  • Ihr Risiko liegt vor allem in Ihrem eigenen Code, etwa in einer Legacy-Anwendung voller rohem SQL. SAST, Reviews und Tests kommen zuerst.
  • Sie bauen außerhalb von GitHub Actions, mit GitLab CI oder Bitbucket Pipelines. CRACI unterstützt heute GitHub Actions; GitLab CI und Jenkins stehen auf der Roadmap.

PHP-Sicherheit: häufig gestellte Fragen

Wie sichere ich PHP-Code ab?

Arbeiten Sie in drei Ebenen. Im Code: Eingaben validieren, Prepared Statements nutzen, sich auf das Ausgabe-Escaping des Frameworks verlassen und Daten aus nicht vertrauenswürdigen Quellen nie deserialisieren. Bei den Abhängigkeiten: composer.lock committen, composer audit im CI ausführen und das Blockieren unsicherer Versionen in Composer aktiviert lassen. Im Build: Actions pinnen, das Token beschränken, den Netzwerkzugriff einschränken und aufzeichnen, was der Job installiert hat.

Was sind die häufigsten PHP-Schwachstellen?

SQL-Injection, Cross-Site-Scripting, Cross-Site Request Forgery, Code Injection über eval(), Object Injection über unserialize(), File Inclusion und unsichere Uploads, Type Juggling mit ==, schwache Session-Einstellungen, SSRF über URL-Wrapper, Command Injection und Mass Assignment. Verwundbare Composer-Abhängigkeiten und ungepatchte PHP-Versionen, etwa mit den PHP2Shell-Fehlern, sind ebenso häufig die Ursache von Vorfällen.

Was ist der beste PHP-Sicherheitsscanner?

Kein einzelnes Tool deckt die PHP-Sicherheit ab. Nutzen Sie einen statischen Analyzer wie PHPStan, Psalm oder Semgrep für Ihren Code, composer audit für Ihre Composer-Abhängigkeiten, einen Container-Scanner für das Image und eine Aufzeichnung aus dem Build für das, was das CI tatsächlich installiert hat.

Wie funktioniert composer audit, und was übersieht es?

Es gleicht installierte Pakete, oder mit --locked die composer.lock, mit Advisories aus der Packagist.org-API ab und meldet aufgegebene und als Malware markierte Pakete. Es endet mit 1, wenn etwas das Audit nicht besteht. PHP selbst, PECL-Extensions, Betriebssystempakete in Ihrem Image und alles, was das CI außerhalb von Composer lädt, sieht es nicht.

Wie blockiere ich unsichere Composer-Pakete im CI?

Nutzen Sie Composer 2.9 oder neuer, das Versionen mit Advisories bei update und require standardmäßig blockiert (policy.advisories.block). Da eine in composer.lock festgeschriebene verwundbare Version trotzdem installiert wird, führen Sie zusätzlich composer audit nach composer install aus und lassen den Job anhand des Exit-Codes fehlschlagen. Übergeben Sie im CI nie --no-blocking.

Ist Laravel standardmäßig sicher?

Größtenteils. Blade escapt die Ausgabe, Eloquent bindet Abfrageparameter, Modelle sind gegen Mass Assignment geschützt und die Session-ID wird beim Login erneuert. Diese Standards greifen nicht mehr, wenn Code {!! !!}, whereRaw() mit Eingaben, $guarded = [] oder APP_DEBUG=true in Produktion nutzt oder wenn der APP_KEY geleakt wird.

Wie aktualisiere ich Abhängigkeiten in einer Legacy-PHP-Anwendung mit wenigen Tests?

Aktualisieren Sie das übergeordnete Paket, das das verwundbare nachzieht, ein Paket nach dem anderen mit composer update vendor/package --with-dependencies. Testen Sie die kritischen Pfade per Smoke-Test, ergänzen Sie Charakterisierungstests rund um das, was das Upgrade berührt, und rollen Sie stufenweise aus.

Wie erstelle ich eine SBOM für ein PHP-Projekt?

Das CycloneDX-Composer-Plugin (cyclonedx/cyclonedx-php-composer) erstellt mit composer CycloneDX:make-sbom eine aus Ihren Composer-Paketen. Sie listet nur Composer-Pakete auf. Eine SBOM aus dem Build hält zusätzlich die PHP-Runtime, Betriebssystempakete und andere Downloads im Build fest.

Was ist PHP2Shell?

PHP2Shell ist der Name für CVE-2026-109618 und CVE-2026-109619, zwei kritische Schwachstellen in PHP selbst, die eine Remote-Codeausführung ohne Authentifizierung ermöglichen können. Behoben sind sie in PHP 8.5.12, 8.4.27, 8.3.36 und 8.2.35, veröffentlicht am 21. Oktober 2026. Siehe PHP2Shell.

Sehen Sie jedes Paket, das Ihr PHP-Build installiert

Lassen Sie einen GitHub-Actions-Workflow auf CRACI laufen und vergleichen Sie die aufgezeichnete SBOM mit Ihrer composer.lock.

Demo buchen