PHP-Entwickler in Augsburg

PHP-Anwendung in Augsburg absichern: PDO mit Platzhaltern gegen SQL-Injection, htmlspecialchars gegen XSS, CSRF-Token, Upload-Prüfung, Sitzungseinstellungen und Sicherheits-Header. So prüfe ich übernommenen Code, schriftlich und remote.

PHP-Entwickler

Mein Platz in der WordPress-Community

  • Mein wordpress.org-Konto besteht seit 2006.
  • Ich habe zum WordPress-Core in den Versionen 7.0 und 7.1 beigetragen.
  • Ich betreue 30 Plugins auf wordpress.org.
  • Ich organisiere WordCamps und trete dort als Speaker auf.
  • Ich war bei jedem WordCamp Europe von 2017 bis 2026 dabei.

Mein WordPress.org-Profil

#Eine übernommene PHP-Anwendung prüfen, bevor Sie etwas ändern

Wer in Augsburg eine PHP-Anwendung von einem früheren Dienstleister oder Mitarbeiter übernimmt, fragt selten zuerst nach der Sicherheit. Es läuft, also bleibt es liegen. Diese Seite beschreibt, was ich in einem solchen Code zuerst suche und wie ich die Funde behebe. Mit der Liste in der Mitte können Sie auch selbst oder mit Ihrem Team anfangen.

Die Stadt Augsburg nennt auf den Seiten ihrer Wirtschaftsförderung als Kompetenzfelder des Produktionsstandorts unter anderem Mechatronik und Automation, Informations- und Kommunikationstechnologie, Künstliche Intelligenz sowie die Sicherheits- und Verteidigungsindustrie (Stand der Seite: 25.08.2026). Für Betriebe in diesen Feldern entstehen oft kleine PHP-Anwendungen neben dem eigentlichen Produkt: ein Anfrageformular, ein Händlerportal, eine Schnittstelle zum Warenwirtschaftssystem. Diese Teile sind aus dem Internet erreichbar, werden selten angefasst und deshalb selten geprüft.

Die Wertach in Augsburg-Pfersee, Luftaufnahme

Foto: Living-wreck, CC0, Wikimedia Commons.

Die OWASP führt in ihrer Top 10 für 2025 Injection auf Platz 5. Die Kategorie umfasst Cross-Site-Scripting und SQL-Injection. Nach den Angaben der OWASP wurde jede getestete Anwendung auf irgendeine Form von Injection geprüft. Der Rest dieser Seite geht die Angriffsarten einzeln durch, jeweils mit dem, was ich im Code finde, und dem, was ich daraus mache.

#SQL-Injection: PDO mit Platzhaltern

Das PHP-Handbuch nennt als empfohlenen Weg, alle Daten über Prepared Statements zu binden. Jeder dynamische Wert in WHERE, SET und VALUES wird durch einen Platzhalter ersetzt, die Daten gehen getrennt vom SQL-Text an die Datenbank. Der Handbuchtext sagt zugleich, dass Parameterbindung nur für Daten gilt. Alles andere, was dynamisch ist, wird gegen eine Liste erlaubter Werte geprüft.

$pdo = new PDO($dsn, $benutzer, $kennwort, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

$richtung = ($_GET['richtung'] ?? '') === 'DESC' ? 'DESC' : 'ASC';
$stmt = $pdo->prepare("SELECT id, name FROM kunden WHERE ort = :ort ORDER BY name $richtung");
$stmt->execute([':ort' => $_GET['ort'] ?? '']);

Der Wert für ort kommt aus der Anfrage und wird gebunden. Die Sortierrichtung kann nicht gebunden werden, deshalb entscheidet der Code selbst, welches der beiden Wörter eingesetzt wird. Mit ERRMODE_EXCEPTION wirft PDO bei Fehlern eine Ausnahme, die der Code behandeln kann, statt still weiterzulaufen.

Im übernommenen Code suche ich die Stellen, an denen ein Text aus $_GET, $_POST oder $_COOKIE direkt in eine Abfrage eingesetzt wird. Das PHP-Handbuch empfiehlt außerdem, sich mit der Datenbank nie als Administrator zu verbinden und stattdessen Benutzer mit minimalen Rechten zu verwenden. Ich prüfe deshalb auch, mit welchem Datenbankbenutzer die Anwendung arbeitet. Für Abfragen mit dynamischen Spalten oder Tabellen lege ich eine feste Zuordnung an, die nur Namen aus dem Code zulässt.

#Cross-Site-Scripting: Ausgabe maskieren

Bei XSS gelangt Text aus einer Eingabe unverändert in eine HTML-Seite und wird dort als Code ausgeführt. Die OWASP nennt als beste Schutzmaßnahmen die Schutzfunktionen des Frameworks, die Kodierung der Ausgabe und das Bereinigen von HTML. In PHP ohne Framework ist htmlspecialchars die Funktion dafür.

function e(string $text): string
{
    return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}

echo '<td>' . e($kunde['name']) . '</td>';

Die Flags sind der Punkt, an dem ältere Anwendungen auffallen. Laut PHP-Handbuch ist der Standard seit PHP 8.1.0 ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401. Davor war er ENT_COMPAT, und der wandelt nur doppelte Anführungszeichen um. Ein Wert in einfachen Anführungszeichen bleibt dann offen. ENT_SUBSTITUTE ersetzt ungültige Zeichenfolgen, statt einen leeren String zurückzugeben. Ich setze die Flags und die Kodierung deshalb immer ausdrücklich und fasse sie in einer kleinen Hilfsfunktion zusammen.

Die Grenze von htmlspecialchars beschreibt die OWASP ebenfalls: Für Attribute mit URLs, CSS oder JavaScript braucht es zusätzliche Maßnahmen, und innerhalb eines Script-Blocks hilft die Funktion nicht. Wo JavaScript Text anzeigt, soll es ihn mit textContent setzen. Eine Content-Security-Policy ergänze ich als zweite Schicht. Die OWASP schreibt dazu, sie sei leicht falsch einzustellen und deshalb nicht die Hauptverteidigung.

#CSRF-Token für jedes Formular, das etwas ändert

Bei einem CSRF-Angriff löst eine fremde Seite im Browser einer angemeldeten Person eine Anfrage an Ihre Anwendung aus. Die OWASP empfiehlt als verbreitetes Muster das Synchronizer-Token: Der Server erzeugt es und prüft es bei jeder ändernden Anfrage. Es kann einmal pro Sitzung oder pro Anfrage erzeugt werden, wobei ein Token je Anfrage die kürzere Angriffsfläche bietet, aber den Zurück-Button stört.

$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
// im Formular: <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">

if (!hash_equals($_SESSION['csrf'] ?? '', (string) ($_POST['csrf'] ?? ''))) {
    http_response_code(403);
    exit;
}

random_bytes liefert laut Handbuch kryptografisch sichere Zufallsbytes. hash_equals vergleicht ohne Zeitunterschied, der etwas über das Token verraten könnte. Wichtig ist die Reihenfolge der Argumente: Zuerst kommt der bekannte Wert, danach die Eingabe des Nutzers. Die Sitzungs-ID selbst eignet sich nach dem Handbuch nicht als Token, weil HTML-Quelltexte gespeichert und weitergegeben werden.

Die OWASP rät außerdem, ändernde Aktionen nie über GET auszuführen. Im Altcode gibt es sie häufig: Ein Link wie loeschen.php?id=17 ist ein CSRF-Fall, noch bevor ein Angreifer ihn bemerkt hat. Ich stelle solche Aktionen auf POST mit Token um.

#Datei-Uploads: Typ, Name und Ablageort

Ein Upload ist die Stelle, an der Fremde etwas auf Ihren Server legen. Die OWASP sagt dazu: Der Content-Type einer hochgeladenen Datei kommt vom Nutzer und lässt sich leicht fälschen. Das PHP-Handbuch zeigt im Beispiel dasselbe in Code, mit dem Hinweis, den Typ selbst zu prüfen und die Datei eindeutig zu benennen.

if (($_FILES['datei']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK || $_FILES['datei']['size'] > 2_000_000) {
    throw new RuntimeException('Upload fehlgeschlagen oder zu groß.');
}
$typ = (new finfo(FILEINFO_MIME_TYPE))->file($_FILES['datei']['tmp_name']);
$endung = array_search($typ, ['jpg' => 'image/jpeg', 'png' => 'image/png', 'pdf' => 'application/pdf'], true);
if ($endung === false) {
    throw new RuntimeException('Dateityp nicht erlaubt.');
}
move_uploaded_file($_FILES['datei']['tmp_name'], sprintf('/var/uploads/%s.%s', bin2hex(random_bytes(16)), $endung));

Der Typ wird aus dem Dateiinhalt bestimmt und gegen eine Liste erlaubter Typen geprüft. Der Dateiname entsteht zufällig, die Endung kommt aus der Liste. Die OWASP warnt vor Tricks wie .jpg.php, Nullbytes und gemischter Groß- und Kleinschreibung in Dateinamen. Ein zufälliger Name nimmt diesen Tricks den Boden. Die Datei liegt im Beispiel außerhalb des Webroots. Die OWASP empfiehlt, Uploads auf einem anderen Server abzulegen und, wenn das nicht geht, außerhalb des Webroots. Für Dateien, die öffentlich abrufbar sein sollen, setzt sie einen Handler in der Anwendung ein, der eine ID auf den Dateinamen abbildet. Die Prüfung der Dateisignatur reicht laut OWASP allein nicht aus, weil sie sich leicht umgehen lässt.

#Sitzungen und Passwörter: das Wichtigste, der Rest steht in Bochum

Sitzungen und Passwortspeicherung habe ich auf der Seite PHP-Entwickler Bochum ausführlich beschrieben, mit Hashing, Rehash, Sperren nach Fehlversuchen und Zwei-Faktor-Anmeldung. Hier nur das, was ich in jedem übernommenen Projekt zuerst nachsehe. Das PHP-Handbuch führt für Sitzungen diese Werte auf:

session.use_strict_mode = 1
session.use_only_cookies = 1
session.use_trans_sid = 0
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Lax"

use_strict_mode nennt das Handbuch für sichere Sitzungen verpflichtend, es ist aber standardmäßig aus. cookie_httponly verhindert, dass JavaScript das Sitzungscookie liest, und mildert damit gestohlene Cookies nach einer JavaScript-Einschleusung. cookie_secure gehört zu jeder Seite, die nur über HTTPS erreichbar ist. use_trans_sid ist ab PHP 8.4.0 als aktiviert veraltet. cookie_samesite gibt es seit PHP 7.3.

Bei Passwörtern prüfe ich, ob password_hash im Einsatz ist. Das Handbuch nennt PASSWORD_DEFAULT als Standard, der derzeit bcrypt verwendet, und empfiehlt, den Hash in einer Spalte zu speichern, die mehr als 60 Bytes fasst, nämlich 255. Findet sich im Code md5( oder sha1( neben einem Passwortfeld, ist das ein Befund für die Seite zu Bochum.

#Sicherheits-Header für die ganze Anwendung

Header setzen Sie am besten am Webserver, damit sie auch für statische Dateien gelten. Wo das nicht geht, setzt sie PHP. Die HTTP-Headers-Liste der OWASP nennt diese Werte:

header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('X-Frame-Options: DENY');

nosniff verhindert, dass der Browser einen anderen Typ errät, als der Server angibt. Die OWASP empfiehlt, den Referrer-Policy-Header ausdrücklich zu setzen, statt sich auf das Standardverhalten der Browser zu verlassen. Gegen das Einbetten in fremde Seiten ist frame-ancestors in der Content-Security-Policy der bevorzugte Weg, X-Frame-Options gilt in der OWASP-Liste als Ersatz. Den Header X-XSS-Protection setzt die OWASP nicht mehr, sie empfiehlt ihn nicht zu setzen oder auf 0 zu stellen. Für HSTS nennt die Liste einen Wert von zwei Jahren, warnt aber, dass ein abgelaufenes Zertifikat Besucher aussperrt, solange die Laufzeit nicht endet. Deshalb setze ich HSTS erst, wenn das Zertifikat automatisch erneuert wird, und anfangs mit kurzer Laufzeit.

Zur Fehlerausgabe sagt das PHP-Handbuch, dass display_errors im Betrieb aus sein soll. Dann gehört log_errors an und error_log auf einen Pfad. So sieht ein Angreifer keine Pfade und Abfragetexte, und Sie haben trotzdem die Meldungen.

#Was das Datenschutzrecht dazu sagt

Artikel 32 DSGVO verlangt geeignete technische und organisatorische Maßnahmen, die dem Risiko angemessen sind, unter Berücksichtigung des Stands der Technik. Genannt werden unter anderem Verschlüsselung, die Fähigkeit, Vertraulichkeit und Integrität der Systeme dauerhaft zu sichern, und ein Verfahren zur regelmäßigen Überprüfung der Wirksamkeit dieser Maßnahmen. Eine PHP-Anwendung mit Kundendaten, die nie geprüft wurde, hat dieses Überprüfungsverfahren nicht.

Zuständig für private Unternehmen in Bayern ist das Bayerische Landesamt für Datenschutzaufsicht (BayLDA). Es führt nach eigener Beschreibung Datenschutzkontrollen vor Ort, mit Fragebögen und als Online-Prüfungen durch, bei denen ein Sachverhalt im Internet kontrolliert wird. Wie ein Verstoß bewertet wird, entscheidet die Behörde im Einzelfall. Ich kann Ihnen dafür die technischen Fakten schriftlich zusammenstellen: welche Schwachstellen es gab, seit wann, was geändert wurde.

#Checkliste für die Prüfung

PrüfpunktWonach ich sucheWas ich ändere
SQLEingaben aus der Anfrage, die direkt in Abfragen stehenPDO-Platzhalter, feste Listen für Sortierung und Namen
DatenbankrechteAnwendung arbeitet als AdministratorBenutzer mit minimalen Rechten
Ausgabeecho und <?= ohne Maskierung, Standard-Flags bei PHP vor 8.1Hilfsfunktion mit ausdrücklichen Flags
CSRFFormulare und GET-Links, die etwas ändernToken, POST statt GET, SameSite
UploadsTyp nach Browser-Angabe, Originalname, Ablage im WebrootTyp aus Inhalt, Zufallsname, Ablage außerhalb
SitzungenWerte in php.ini und im CodeWerte wie oben, Details auf der Bochum-Seite
Passwörtermd5(, sha1( oder Klartextpassword_hash, Rehash beim Login
Headerfehlende Header, X-XSS-Protection auf 1Werte aus der OWASP-Liste
Fehlerdisplay_errors im Betrieb anaus, Protokoll an
Paketeveraltete Composer-Paketecomposer audit, Updates auf Staging

#Wie ein Auftrag mit mir abläuft

  1. Schriftliche Anfrage. Sie beschreiben die Anwendung, die PHP-Version und was Sie beunruhigt.
  2. Rückfragen per E-Mail. Ich frage nach Framework, Datenbank und danach, wo die Anwendung läuft.
  3. Lesen des Codes. Mit Zugang zum Repository und zu einer Staging-Kopie gehe ich die Tabelle oben durch und halte jeden Befund mit Datei und Zeile fest.
  4. Umfang und Annahmen. Sie erhalten eine schriftliche Liste: was ich ändere, was nicht, und welche Entscheidungen bei Ihnen liegen.
  5. Umsetzung auf Staging. Jede Änderung läuft zuerst dort, mit Tests für die betroffenen Formulare und Abfragen.
  6. Abnahme und Übergabe. Nach Ihrer Freigabe geht es live. Auf Wunsch bekommen Sie eine Beschreibung der Maßnahmen für Ihre Datenschutzunterlagen und ein Wartungsangebot für die Zeit danach.

Das Aussehen neuer oder geänderter Formulare kommt von Ihnen, Layout und Wireframes ebenso. Ich setze sie technisch um. Ich arbeite allein, ausschließlich remote und schriftlich, ohne Besprechungen und Telefonate.

#Was ich von Ihnen brauche

  • Lesezugriff auf den Quellcode, als Repository oder als Archiv.
  • Eine Staging-Kopie der Anwendung. Fehlt sie, richte ich sie mit Ihrem Hoster ein.
  • Die PHP-Version und die Ausgabe von phpinfo() vom Server, damit ich die Sitzungswerte sehe.
  • Eine Ansprechperson für Datenschutzfragen.
  • Falls vorhanden: frühere Prüfberichte und die Dokumentation Ihrer technischen und organisatorischen Maßnahmen.

#Häufige Fragen

#Reicht htmlspecialchars an jeder Stelle, an der ich Daten ausgebe?

Für Text zwischen HTML-Tags und für Werte in Anführungszeichen ja, mit den richtigen Flags. Nicht für URLs, Inline-JavaScript, CSS oder HTML-Kommentare. Die OWASP nennt diese Stellen gefährliche Kontexte, die eigene Maßnahmen brauchen. Ich suche sie im Code einzeln.

#Genügen Prepared Statements allein gegen SQL-Injection?

Nicht ganz. Das PHP-Handbuch sagt, dass Parameterbindung nur für Daten gilt. Sortierrichtung, Spaltennamen und Tabellennamen müssen gegen eine feste Liste erlaubter Werte geprüft werden. Außerdem schützt die Abfrage nicht mehr, wenn der Text vor dem Vorbereiten zusammengesetzt wird.

#Brauche ich CSRF-Token, wenn das Sitzungscookie SameSite trägt?

Ja, ich setze beides. Das PHP-Handbuch beschreibt SameSite als Abschwächung von CSRF. Die OWASP führt es als zusätzliche Schicht neben dem Token und verlangt mindestens eine Maßnahme aus dieser Gruppe.

#Wo müssen hochgeladene Dateien liegen?

Nach Möglichkeit auf einem anderen Server, sonst außerhalb des Webroots. Der Name wird zufällig vergeben, der Typ gegen eine Liste erlaubter Typen geprüft. Der Browser-Typ aus $_FILES wird nicht beachtet.

#Muss ich einen erfolgreichen Angriff melden?

Artikel 33 DSGVO verlangt die Meldung an die Aufsichtsbehörde unverzüglich und möglichst binnen 72 Stunden, wenn die Verletzung voraussichtlich ein Risiko für Betroffene bedeutet. Wann das der Fall ist, klären Sie mit Ihrer Datenschutzbeauftragten oder Ihrem Datenschutzbeauftragten. Ich liefere die technischen Fakten aus den Protokollen.

#Prüfen Sie auch die Composer-Pakete?

Ja. Der Befehl composer audit vergleicht die installierten Pakete mit bekannten Sicherheitshinweisen und meldet aufgegebene Pakete. Die OWASP führt veraltete Komponenten als eigene Kategorie, Software Supply Chain Failures, auf Platz 3 der Top 10 2025.

#Schreiben Sie mir

Wenn in Ihrer Anwendung Formulare, Uploads oder ein Kundenbereich laufen, die nie jemand auf diese Punkte geprüft hat, schicken Sie mir über die Kontaktseite ein paar Zeilen: die Adresse der Anwendung oder eine kurze Beschreibung, Ihr Ziel, eine Frist, falls es eine gibt, und den Stack mit PHP-Version und Framework. Ich antworte schriftlich auf Werktagen mit einer ersten Einschätzung und den offenen Fragen. Diese Einschätzung ist kostenlos.

Einen Überblick über meine PHP-Leistungen finden Sie unter PHP-Entwickler. Wie ich PHP-Projekte in Bayern angehe, zeigen auch die Seiten für München und Nürnberg. Für die laufende Pflege einer bestehenden Seite gibt es die WordPress-Wartung in Augsburg.

Brauchen Sie die Leistung: PHP-Entwickler in Augsburg?

Beschreiben Sie Ihr Projekt per E-Mail. Antwort innerhalb von 48 Stunden.

Projekt beschreiben

Weitere WordPress-Dienste und Wissensbasis entdecken