Startseite  /  Blog  /  PHP-Formular sicher einbinden
Ratgeber · Technik

PHP-Kontaktformular auf kleinen Websites sicher einbinden

Von Steven Petter · Lesezeit ca. 8 Minuten

Eine kleine Unternehmenswebsite braucht selten mehr als ein Kontaktformular. Ohne Baukasten und ohne WordPress reicht dafür ein PHP-Skript neben den Seiten: Es nimmt die Eingaben an und schickt sie als Mail weiter. Das sind ein paar Dutzend Zeilen, und jede davon ist von außen erreichbar, auch für Programme, die das Formular nie gesehen haben.

Auf dieser Website läuft genau so ein Skript. Im Oktober 2026 wurde dann eine kritische Lücke in PHP selbst bekannt, gegen die auch ein sorgfältig geschriebenes Skript nichts ausrichtet.

Was in das Skript gehört

Der Browser prüft Pflichtfelder und Mailformat schon vor dem Absenden. Darauf verlassen kannst Du Dich nicht. Wer das Skript direkt anspricht, umgeht das Formular und mit ihm jede Prüfung im Browser. Das Skript wiederholt deshalb alles auf dem Server.

Angenommen werden nur Anfragen per POST. Wer die Adresse im Browser aufruft, bekommt 405.

Das Skript prüft jedes Pflichtfeld, die Mailadresse mit der PHP-Funktion filter_var und ebenso das Häkchen bei der Datenschutzerklärung. Fehlt etwas, geht eine Fehlermeldung zurück. Eine Mail entsteht dann nicht.

Antworten gibt das Skript nur als feste Texte. So kann nichts, was jemand eingetippt hat, als Code in der Seite landen.

Die Nachricht selbst speichert es nicht, sie geht direkt als Mail ins Postfach. Was nirgends liegt, kann bei einem Einbruch auch nicht abfließen.

Dazu kommt ein Fallenfeld. Im Formular steht ein zusätzliches Feld, das Besucher nicht sehen. Einfache Spamprogramme füllen jedes Feld aus, das sie finden. Ist es gefüllt, meldet das Skript trotzdem Erfolg und verschickt nichts. Der Absender soll ja nicht merken, dass er aufgefallen ist.

Der Mailkopf ist die Stelle, an der es kippt

PHP verschickt die Mail mit der Funktion mail(). Absender, Antwortadresse und weitere Angaben stehen dabei als Textzeilen im Kopf der Mail, getrennt durch Zeilenumbrüche. Schreibt das Skript den Namen aus dem Formular ungeprüft dort hinein, kann ein Angreifer im Namensfeld einen Zeilenumbruch mitschicken und dahinter eine eigene Zeile, etwa eine Blindkopie an tausend fremde Adressen. Dein Server verschickt dann Spam in Deinem Namen. Bevor die Mail entsteht, entfernt das Skript deshalb jeden Zeilenumbruch aus allem, was im Kopf landet.

Als Absender steht ein echtes Postfach Deiner eigenen Domain im Kopf. Die Adresse des Besuchers gehört in die Antwortadresse, das Feld Reply-To, damit Du mit einem Klick antworten kannst. Trägt ein Skript stattdessen die Besucheradresse als Absender ein, verschickt Dein Server Mails im Namen fremder Domains. Empfangende Mailserver prüfen über SPF und DMARC, ob ein Server für eine Domain senden darf. Fällt die Prüfung durch, landet die Anfrage im Spam oder kommt gar nicht an.

Was PHP2Shell geändert hat

Im Oktober 2026 wurden zwei kritische Lücken in PHP vor dem geplanten Termin öffentlich: CVE-2026-109618 und CVE-2026-109619, zusammen PHP2Shell genannt. Betroffen ist laut Sansec jede ungepatchte Version seit PHP 5.4. Der Fehler sitzt dort, wo PHP die eingehende Anfrage zerlegt, und das geschieht, bevor Dein Skript die erste Zeile ausführt. Wie sauber das Formularskript geschrieben ist, spielt dafür keine Rolle.

Die eigentliche Abhilfe ist das Update auf PHP 8.2.35, 8.3.36, 8.4.27 oder 8.5.12. Für 8.1 und älter bringt PHP selbst keine Updates mehr heraus, dort helfen nur Distributionen und Hoster, die den Fix nachrüsten. Bei Shared Hosting liegt das beim Anbieter, Dir bleibt nur Nachfragen oder ein Blick auf seine Statusseite. Bei mir kam die Bestätigung am 22. Oktober mittags, einen Tag nachdem ich die Sperre aus dem nächsten Abschnitt gesetzt hatte.

Für die Zeit bis dahin hat Sansec eine Überbrückung veröffentlicht. Alle bekannten Angriffe kamen als Anfrage vom Typ multipart/form-data. In diesem Format laden Formulare Dateien hoch, ein Kontaktformular ohne Anhang braucht es nicht. Drei Zeilen ganz oben in der .htaccess weisen jede solche Anfrage ab, bevor PHP sie zu sehen bekommt:

RewriteEngine On
RewriteCond %{HTTP:Content-Type} multipart/form-data [NC]
RewriteRule ^ - [F]

Der Zusatz [NC] lässt die Regel unabhängig von Groß- und Kleinschreibung greifen, denn die Schreibweise wählt der Angreifer. Abgewiesene Anfragen bekommen den Status 403 direkt vom Webserver.

Bevor Du die Regel setzt, prüf, wie Dein eigenes Formular sendet. Dieses hier schickt JSON und war von der Sperre nie betroffen. Bei einer zweiten Seite, die ich betreue, sah das anders aus. Dort baute das Formular seine Anfrage mit new FormData, und das ergibt genau multipart/form-data. Mit der Sperre wäre jede Anfrage abgewiesen worden. Eine Änderung im JavaScript stellt auf das schlichtere Formularformat um, das PHP genauso über $_POST liest:

body: new URLSearchParams(new FormData(form))

Am Skript selbst ändert sich dabei nichts.

Nach dem Hochladen prüfst Du vier Dinge: Die Seiten laden normal. Ein Aufruf des Skripts im Browser bekommt 405. Eine Testanfrage im Format Deines Formulars kommt durch. Eine Anfrage als multipart wird mit 403 abgewiesen, auch in Großbuchstaben geschrieben. Für die Testanfrage lohnt sich das Fallenfeld: Ist es gefüllt, läuft die Anfrage komplett durch, und trotzdem geht keine Mail hinaus.

Sansec rät, die Regel nach dem Update wieder zu entfernen, weil sie Datei-Uploads blockiert. Auf einer Seite ohne Uploads stört sie nicht. Bei mir steht sie noch.

Was die Sperre nicht abdeckt

Wie Dein Formular sendet, ist einem Angreifer egal. Er wählt das Format selbst.

Abstract Security beschreibt eine Variante der ersten Lücke, die ohne multipart auskommt. Sie schickt eine mehrere Megabyte große Anfrage im gewöhnlichen Formularformat mit einem übergroßen Feldnamen, und die multipart-Sperre lässt sie durch. Ein Kontaktformular kommt mit wenigen Kilobyte aus. Die Größe einer Anfrage lässt sich in der .htaccess begrenzen, sofern Dein Hoster die Einstellung dort zulässt:

LimitRequestBody 102400

Das sind 100 KB, alles darüber weist der Webserver ab. Ob die Grenze greift, zeigt wieder eine Testanfrage, diesmal mit mehr als 100 KB.

Das Update ersetzen kann keine dieser Regeln, darauf weisen Sansec und Abstract Security ausdrücklich hin.

Zeitfalle und Grenze je Absender

Auch das Fallenfeld hat eine Grenze. Ein Programm, das jemand gezielt für Deine Seite schreibt, lässt das versteckte Feld leer.

Naheliegend wäre hier ein Captcha. Ich habe mich dagegen entschieden. Das verbreitete von Google lädt Code nach und schickt Daten an Google, und meine Seite kommt bewusst ohne fremde Dienste aus. Außerdem ist jedes Rätsel vor dem Absenden eine Hürde mehr für alle, die mir schreiben wollen.

Stattdessen prüft das Skript zwei Dinge, von denen Besucher nichts merken.

Das erste ist die Zeit. Ein Mensch braucht für Name, Mailadresse und Nachricht länger als drei Sekunden, ein Programm nicht. Das Formular schickt deshalb mit, wie viele Sekunden seit dem Laden der Seite vergangen sind:

dauer: Math.round(performance.now() / 1000)

Im Skript reichen dann wenige Zeilen. Wer zu schnell war, bekommt wie beim Fallenfeld eine Erfolgsmeldung, und es geht keine Mail hinaus:

$dauer = field($data, 'dauer');
if ($dauer !== '' && is_numeric($dauer) && (float) $dauer < 3) {
    echo json_encode(['success' => true, 'message' => 'Danke!']);
    exit;
}

Fehlt der Wert ganz, lässt das Skript die Anfrage durch. Sonst gingen nach dem Update die Nachrichten von Besuchern verloren, die die Seite noch in der alten Fassung offen hatten. Ein Programm, das den Wert selbst setzt, kommt allerdings durch.

Das zweite ist die Menge. Von einem Anschluss nimmt das Skript höchstens fünf Anfragen pro Stunde an, die sechste bekommt den Status 429 und den Hinweis, direkt per Mail zu schreiben. Gezählt wird erst nach allen anderen Prüfungen, sonst säße fest, wer mehrmals ein Pflichtfeld vergisst.

Dafür muss sich das Skript merken, wer schon geschrieben hat. Die IP-Adresse selbst speichert es nicht, es bildet daraus mit einem geheimen Schlüssel einen Prüfwert:

$key  = file_get_contents($dir . '/schluessel');
$file = $dir . '/' . hash_hmac('sha256', $ip, $key) . '.log';

In der Datei stehen nur die Zeitpunkte der Anfragen. Der Ordner liegt eine Ebene über dem Webordner und ist über keine Adresse erreichbar. Den Schlüssel erzeugt das Skript beim ersten Aufruf selbst, er steht also in keinem Quelltext. Bei IPv6 zählt nur der vordere Teil der Adresse, das sogenannte /64-Netz, denn ein Gerät kann innerhalb dieses Netzes laufend neue Adressen verwenden. Geht beim Speichern etwas schief, sperrt das Skript nicht, damit ein Fehler am Server keine echten Anfragen kostet.

Auch der Prüfwert ist ein personenbezogenes Datum, denn mit dem Schlüssel lässt er sich jeder Adresse wieder zuordnen. Was gespeichert wird, wofür, wie lange und auf welcher Rechtsgrundlage, gehört deshalb in die Datenschutzerklärung.

Was in jedes Formularskript gehört
  • →Nur POST annehmen, alles andere mit 405 abweisen
  • →Jede Prüfung aus dem Browser auf dem Server wiederholen
  • →Zeilenumbrüche aus allem entfernen, was in den Mailkopf geht
  • →Absender aus der eigenen Domain, Besucheradresse nur als Antwortadresse
  • →Feste Antworttexte, keine Eingabe an die Seite zurückgeben
  • →Nichts speichern, was nicht gebraucht wird
  • →Ein unsichtbares Fallenfeld gegen einfachen Spam
  • →Eine Zeitfalle und eine Grenze je Absender gegen gezielte Programme

Quellen

Sansec: PHP2Shell, Analyse vom 21.10.2026 mit Nachtrag vom 22.10.2026. Abstract Security: PHP2Shell, technische Beschreibung beider Lücken samt der Variante ohne multipart. Dokumentation zu LimitRequestBody, Apache HTTP Server 2.4.

Wie eine kleine Website technisch und inhaltlich aufgebaut sein sollte, steht auf meiner Seite zu Website-Konzeption & SEO. Wo Dein Formular steht, schauen wir uns im Erstgespräch an.

Läuft Dein Formular sicher?

Lass uns Skript und Server zusammen durchgehen, bevor es jemand anders tut.

Erstgespräch anfragen
P. MARKETING
Unpacking...