Die Security-Audit-Checkliste für KI-generiertes PHP: 7 Schwachstellen-Muster in jeder Vibe-Coded-Codebase

#KI-generiertes PHP Sicherheit
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Wenn du in den letzten zwei Jahren ein PHP-Projekt mit einem nennenswerten Anteil an KI-generiertem Code ausgeliefert hast, besteht eine realistische Chance, dass es mindestens drei der hier beschriebenen Muster enthält. Das ist keine Vermutung - es ist das, was wir bei Wolf-Tech in nahezu jedem Codebase-Audit für Teams finden, die ihre Delivery mit KI-Assistenten beschleunigt haben. Die Schwachstellen sind nicht zufällig. Sie sind vorhersehbar, weil sie aus vorhersehbaren Lücken in der Art entstehen, wie Large Language Models PHP generieren.

Dieser Beitrag arbeitet die sieben Muster durch, erklärt den zugrunde liegenden Grund, warum LLMs sie produzieren, und gibt dir die Static-Analysis-Regeln - GrumPHP-Hooks, PHPStan-Extensions, Rector-Regeln -, die sie in der CI abfangen, bevor sie die Produktion erreichen.

Warum KI-generiertes PHP eine eigene Sicherheitssignatur hat

Sprachmodelle generieren Code, indem sie vorhersagen, was angesichts allem, was sie in den Trainingsdaten gesehen haben, korrekt aussieht. Sicherheitslücken sind in Trainingskorpora aus zwei Gründen unterrepräsentiert. Erstens taucht verwundbarer Code selten mit einem Label auf, das sagt "das hier ist verwundbar". Er steht neben allem anderen im Repository. Zweitens sind die häufigsten Beispiele jedes Musters - eine Datenbankabfrage, ein Dateizugriff, ein Redirect - schnell geschrieben, ohne die Härtungsschichten, die ein erfahrener Entwickler in einem Produktionskontext hinzufügen würde.

Das Ergebnis ist Code, der eine oberflächliche Durchsicht besteht und in einem Security-Audit durchfällt. Die sieben Muster unten sind diejenigen, die in jeder Codebase auftauchen, ungefähr in dieser Reihenfolge der Häufigkeit.

Muster 1: Rohe SQL-Strings direkt neben ->execute()-Aufrufen

Der häufigste Befund ist SQL-Injection durch String-Interpolation oder -Konkatenation in eine Query, die dann direkt an einen PDO- oder Doctrine-execute()-Aufruf übergeben wird. Das LLM weiß, wie man parametrisierte Queries schreibt - es produziert sie, wenn man explizit danach fragt. Ohne diese Aufforderung nimmt es den kürzeren Weg, der dem dominanten Muster in seinen Trainingsdaten entspricht: die Variable direkt zu interpolieren.

// Was KI häufig generiert
$query = "SELECT * FROM users WHERE email = '$email' AND status = 'active'";
$stmt = $pdo->query($query);

// Was sie generieren sollte
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? AND status = 'active'");
$stmt->execute([$email]);

Die PHPStan-Extension, die das abfängt, ist phpstan/phpstan-strict-rules in Kombination mit einer Custom Rule, die String-Interpolation in jedem Ausdruck markiert, der an query(), exec() oder execute() übergeben wird. Wenn du Doctrine verwendest, markiert die Doctrine-Extension für PHPStan (phpstan/phpstan-doctrine) rohe DQL-Konstruktion auf ähnliche Weise.

In GrumPHP fügst du einen phpstan-Task auf Level 6 oder höher mit aktivierter Strict-Rules-Extension hinzu. Das fängt das Muster zum Commit-Zeitpunkt ab statt erst im Review.

Muster 2: Fehlende CSRF-Tokens bei eigenen Formular-Submissions

Symfonys Security-Komponente behandelt CSRF korrekt, wenn du den Form Builder verwendest. KI-generierter Code umgeht den Form Builder häufig bei einfachen Formularen - Kontaktformulare, Präferenz-Toggles, Inline-Edit-Controls - und generiert ein einfaches HTML-Formular mit einem POST-Endpoint, der nichts außer den Feldwerten validiert.

// KI-generierter Controller - kein CSRF-Check
public function updatePreferences(Request $request): Response
{
    $user->setNewsletterEnabled($request->request->getBoolean('newsletter'));
    $this->entityManager->flush();
    return new JsonResponse(['success' => true]);
}

Der Fix ist eine Zeile mit Symfonys CSRF-Manager, aber die KI fügt sie nicht hinzu, weil CSRF-Tokens nicht Teil des grundlegenden POST-Endpoint-Musters sind, gegen das sie matcht. In Rector kannst du eine Custom Rule schreiben, die jeden $request->request->get()- oder $request->request->getBoolean()-Aufruf in einer Controller-Action markiert, der keinen zugehörigen $this->isCsrfTokenValid()-Aufruf hat. Sie fängt nicht jeden Fall, aber sie macht die am stärksten exponierten Endpoints sofort sichtbar.

Muster 3: Nutzerkontrollierte Pfade in Dateioperationen

Path-Traversal-Schwachstellen entstehen, wenn KI-generierter Code einen Dateinamen oder ein Pfadsegment aus Nutzereingaben akzeptiert und ihn - manchmal mit einem realpath-Aufruf, manchmal ohne - an file_get_contents, readfile, unlink oder ähnliche Funktionen übergibt. Der realpath-Aufruf vermittelt falsche Sicherheit: Er löst den Pfad auf, erzwingt aber nicht, dass er innerhalb des vorgesehenen Verzeichnisses bleibt.

// KI-generiert - Traversal trotz realpath möglich
$path = realpath('/var/app/uploads/' . $request->query->get('file'));
return new BinaryFileResponse($path);

Ein Angreifer kann ../../../etc/passwd als file-Parameter übergeben. Das korrekte Muster ist, den Pfad aufzulösen und dann zu prüfen, dass er mit dem erlaubten Präfix beginnt.

PHPStan mit dem Paket thecodingmachine/phpstan-safe-rule markiert unsichere Verwendung von Dateisystemfunktionen. Eine zusätzliche Custom PHPStan Rule, die vor jeder File-Response einen str_starts_with($path, $allowedBasePath)-Guard verlangt, deckt das systematisch über die gesamte Codebase ab. In Wolf-Tech-Audits hat allein diese Regel bei vier von fünf geprüften File-Serving-Features Path-Traversal-Risiken aufgedeckt.

Muster 4: Hartcodierte Secrets in .env.example, die in Git gelandet sind

Dieses Muster ist subtil. Die KI-generierte .env.example enthält häufig echt aussehende Platzhalterwerte - JWT-Secrets, die tatsächlich vom Modell generierte 32-Zeichen-Zufallsstrings sind, API-Keys, die exakt wie echte Keys formatiert sind, Datenbankpasswörter mit glaubwürdiger Entropie. Manche Teams kopieren .env.example in der CI nach .env und ändern die Werte nie. Andere verwenden die Beispielwerte als Staging-Credentials.

Das Problem verschärft sich, wenn .env.example mit Werten committet wird, die dem entsprechen, was die KI auch in config/packages/security.yaml als Fallback platziert hat. Der "Platzhalter" ist jetzt ein echtes Credential in der Versionshistorie.

GrumPHPs git_blacklist-Task mit Patterns für JWT-Secret-Formate, AWS-Key-Muster (AKIA[A-Z0-9]{16}) und High-Entropy-Strings ([A-Za-z0-9+/]{32,}) fängt die meisten davon ab, bevor sie ins Repository gelangen. Ergänze das mit trufflesecurity/trufflehog als CI-Step auf jedem Pull Request. Es ist schnell, kostenlos für öffentliche Repositories, und seine False-Positive-Rate auf PHP-Codebases ist niedrig.

Muster 5: Überprivilegierte JWT-Claims ohne Expiry-Validierung

KI-generierter JWT-Authentifizierungscode implementiert das Encoding und Decoding tendenziell korrekt und überspringt dann den Validierungsschritt, der die Constraints tatsächlich durchsetzt. Dem dekodierten Token wird vertraut, wenn die Signatur gültig ist. Das Ablaufdatum (exp) wird nicht geprüft. Rollen-Claims werden direkt aus dem Token gelesen, ohne Abgleich mit der Datenbank.

// KI-generierter JWT-Handler - Signatur geprüft, Claims nicht validiert
$decoded = JWT::decode($token, new Key($secret, 'HS256'));
$userId = $decoded->sub;
$role = $decoded->role; // wird ungeprüft übernommen, nie gegengeprüft

Ein Angreifer, der irgendein gültiges Token erlangt - auch ein abgelaufenes aus einer widerrufenen Session -, kann den role-Claim ändern, mit demselben Secret neu signieren, falls es exponiert wurde (siehe Muster 4), und Privilegien eskalieren. Der Fix umfasst die Validierung von exp und iat sowie den Abgleich von Rollen-Claims mit der Datenbank bei Aktionen, die erhöhte Rechte erfordern.

PHPStan kann fehlende Business-Logik nicht zuverlässig erkennen, aber ein eigener Architekturtest über einen Service-Interface-Contract funktioniert: Definiere ein JwtValidatorInterface, das validateExpiry()- und validateClaims()-Methoden verlangt, und stelle in der CI sicher, dass jede Klasse, die JWT-Decode implementiert, auch dieses Interface implementiert.

Muster 6: Open Redirects durch KI-angehängte Query-Parameter

Redirect-after-Login ist ein Standardmuster. KI-generierte Implementierungen hängen die Ziel-URL häufig als rohen Query-Parameter an und leiten ohne Validierung dorthin weiter.

// KI-generiert - Open Redirect
$returnUrl = $request->query->get('return_url', '/dashboard');
return new RedirectResponse($returnUrl);

Ein Phishing-Angriff übergibt return_url=https://evil.example.com im Login-Link. Der Nutzer authentifiziert sich auf deiner Domain und landet auf der Seite des Angreifers, im Glauben, sich noch in deiner Anwendung zu befinden.

Symfonys UrlHelper löst das nicht. Der Fix ist die Prüfung, dass das Redirect-Ziel ein relativer Pfad ist oder einer Allowlist vertrauenswürdiger Domains entspricht. Eine Custom PHPStan Rule, die new RedirectResponse($variable) markiert, wenn $variable aus $request->query stammt und kein Validierungsaufruf vorausgeht, macht jede Instanz in der Codebase in einem einzigen Lauf sichtbar.

Muster 7: Symfony-Firewall-Bypässe durch falsch geordnete access_control-Regeln

Symfonys access_control-Regeln werden von oben nach unten ausgewertet und brechen beim ersten Match ab. KI-generierte security.yaml-Dateien platzieren breite Regeln häufig vor spezifischen, weil das Modell ein Template in der Reihenfolge ausfüllt, die sich in Prosa am natürlichsten liest, nicht in der Reihenfolge, in der Symfony Regeln auswertet.

# KI-generiert - das Admin-Panel ist ohne Authentifizierung erreichbar
access_control:
    - { path: ^/api, roles: PUBLIC_ACCESS }
    - { path: ^/api/admin, roles: ROLE_ADMIN }

Die /api/admin-Regel wird nie erreicht, weil /api zuerst matcht und öffentlichen Zugriff gewährt. Die korrekte Reihenfolge platziert die spezifischsten Pfade zuerst.

PHPStan kann YAML-Semantik nicht linten, aber eine Rector-Regel, die auf dem geparsten security.yaml-AST operiert, kann Ordnungs-Constraints durchsetzen. Alternativ fängt ein Architekturtest, der die tatsächliche Symfony-Security-Konfiguration lädt und prüft, dass jeder Pfad mit admin im Pattern eine restriktivere Rolle hat als jeder übergeordnete Pfad, das zuverlässig in der CI ab.

Alles zusammen: Die CI-Pipeline, die alle sieben abfängt

Ein praktikables CI-Gate, das diese Muster vor dem Merge abfängt, besteht aus vier Schichten:

GrumPHP läuft beim Commit: phpstan auf Level 6 mit Strict Rules, git_blacklist mit Secret-Patterns und composer validate. Das dauert unter zehn Sekunden und stoppt die offensichtlichen Muster, bevor sie ins Repository gelangen.

PHPStan läuft in der CI auf jedem Pull Request mit Custom Rules für die obigen Muster. Das Custom-Rules-Verzeichnis kann in tools/phpstan/src/ liegen und über phpstan.neon registriert werden. Ziel sind null ignorierte Fehler - eine Baseline-Datei, die über die Zeit wächst, ist technische Schuld, die echte Befunde still absorbiert.

Rector läuft in der CI im Dry-Run-Modus und produziert einen Diff. Ist der Diff nicht leer, schlägt der Build fehl. Das macht Refactoring-Gelegenheiten sichtbar, ohne sie zu blockieren, falls das Team sie getrennt vom PR einplanen muss.

TruffleHog läuft als GitHub-Actions-Step auf jedem PR, nur gegen den Diff statt der vollen Historie, aus Geschwindigkeitsgründen.

Für Teams, die diese Tools zum ersten Mal auf einer KI-unterstützten Codebase ausführen, produziert der erste PHPStan-Lauf auf Level 6 typischerweise zwischen 200 und 600 Fehler. Diese Zahl ist kein Urteil - sie spiegelt die Lücke zwischen Trainingsdaten-PHP und produktionsreifem PHP wider. Sie systematisch zu triagieren ist genau das, was ein Code-Audit liefert: eine priorisierte Liste von Befunden mit Schweregraden und einer Behebungsreihenfolge, die das Regressionsrisiko begrenzt.

Der Startpunkt

Wenn du eine PHP-Codebase mit signifikantem KI-generiertem Anteil hast und noch kein strukturiertes Security-Audit durchgeführt hast, sind die sieben Muster oben eine vernünftige erste Checkliste. Jedes hat eine erkennbare Signatur, einen mechanischen Fix und eine CI-Regel, die ein Wiederauftreten verhindert.

Die Teams, die das gut handhaben, behandeln es als einmaligen Triage-Durchgang, gefolgt von einem CI-Gate, das das Muster strukturell unmöglich macht. Die korrekte Instrumentierung dauert typischerweise wenige Tage, die Abarbeitung des Backlogs einen Sprint.

Wenn du einen externen Blick auf die Befunde möchtest, bevor du priorisierst - oder wenn das Audit etwas zutage fördert, bei dem dein Team unsicher ist, wie es vorgehen soll -, melde dich unter hello@wolf-tech.io oder besuche wolf-tech.io. Wir haben dieses Audit oft genug durchgeführt, dass die häufigen Muster schnell triagiert sind und die Edge Cases das sind, was tatsächlich Zeit kostet.