Kontaktformular ohne Spam und ohne CAPTCHA: 6 Schutzmaßnahmen

So stoppen Sie Spam im Formular ohne CAPTCHA: Bot-Falle, Zeit-Ticket, Limits und Validierung. PHP-Code zum Kopieren und der Fehler, der selbst Spam verschickt.

Illustration: ein Kontaktformular mit einem blauen Schild, an dem kleine Roboter abprallen
KI-generierte Illustration

Das Kontaktformular ist der einfachste Weg, auf dem Ihre Kundschaft zu Ihnen findet, und zugleich das Lieblingsziel von Bots. Spam im Postfach ist noch das kleinere Übel. Schlimmer ist es, wenn ein schlecht geschriebenes Formular selbst anfängt, Spam von Ihrer Domain zu verschicken, und Ihre E-Mails dann in den „Spam“-Ordnern Ihrer Kundschaft landen.

Die häufigste Antwort auf Spam ist CAPTCHA: Markieren Sie die Bilder mit Hydranten. Das funktioniert, hat aber seinen Preis. Ein Teil der Leute verzichtet darauf, die Nachricht zu senden, und reCAPTCHA übermittelt Daten der Besuchenden an Google. In diesem Artikel zeige ich 6 Schutzmaßnahmen, die Ihre Kundschaft gar nicht bemerkt. So funktioniert das Formular auf meiner Website und in den Projekten, die ich für meine Kundschaft umsetze.

1. Bot-Falle (Honeypot)

Bots füllen alle Felder aus, die sie im Code finden. Sie fügen also ein Feld hinzu, das ein Mensch nicht sieht. Kommt es ausgefüllt an, hat es ein Bot gesendet:

<div class="hp" aria-hidden="true">
  <label for="www">Dieses Feld bitte leer lassen</label>
  <input id="www" name="website" type="text" tabindex="-1" autocomplete="off">
</div>
.hp { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
if (trim($_POST['website'] ?? '') !== '') {
    // wir täuschen Erfolg vor: Der Bot erfährt nicht, dass er in die Falle geraten ist, und versucht es nicht weiter
    echo json_encode(['ok' => true]);
    exit;
}

Drei Details machen den Unterschied. Das Feld heißt ganz gewöhnlich („website“) und nicht „honeypot“. tabindex="-1" sorgt dafür, dass jemand, der sich mit der Tastatur bewegt, nicht hineingerät. aria-hidden verbirgt es vor Screenreadern. Verwenden Sie nicht display: none, denn ein Teil der Bots überspringt solche Felder.

2. Zeit-Ticket

Ein Mensch braucht einige Dutzend Sekunden, um ein Formular auszufüllen. Ein Bot sendet es in einem Bruchteil einer Sekunde, oft ohne die Seite zu laden. Beim Laden des Formulars geben Sie daher ein Ticket mit Zeit und Signatur aus, und beim Absenden prüfen Sie, ob es echt ist und wie viele Sekunden alt:

const SEKRET = 'langer Zufallsstring, z. B. bin2hex(random_bytes(32)), außerhalb des Codes gespeichert';

function bilet_nowy(): string
{
    $t = (string) time();
    return $t . '.' . hash_hmac('sha256', $t, SEKRET);
}

function bilet_ok(string $bilet): bool
{
    if (!preg_match('/^(\d{10})\.([a-f0-9]{64})$/', $bilet, $m)) return false;
    if (!hash_equals(hash_hmac('sha256', $m[1], SEKRET), $m[2])) return false;   // gefälscht
    $wiek = time() - (int) $m[1];
    return $wiek >= 3 && $wiek <= 7200;   // nicht schneller als in 3 s und nicht später als nach 2 h
}

Das Ticket können Sie als verstecktes Feld ins Formular einsetzen. Ist die Seite statisch oder liegt sie im Cache, rufen Sie es per Skript von einer separaten Adresse ab, sobald jemand beginnt, das Formular auszufüllen. Ein zusätzlicher Vorteil: Ein Bot ohne JavaScript bekommt es gar nicht. Die HMAC-Signatur sorgt dafür, dass man einem Bot nicht beibringen kann, „trage die Zeit von vor einer Minute ein“, denn ohne das Geheimnis kann er keine gültige Signatur erzeugen.

3. Limits für Einsendungen

Auch ein raffinierter Bot hat begrenzte Zeit. Ein Limit pro Verbindung, zum Beispiel 5 Nachrichten in 10 Minuten, und ein gemeinsames Tageslimit schützen das Postfach vor einer Flut:

function limit_ok(string $ip, int $max = 5, int $okno = 600): bool
{
    // Hash der IP statt der Adresse selbst: Sie speichern keine personenbezogenen Daten
    $plik = sys_get_temp_dir() . '/formularz-' . hash('sha256', $ip . SEKRET) . '.json';
    $fh = fopen($plik, 'c+');
    flock($fh, LOCK_EX);   // zwei gleichzeitige Einsendungen überschreiben den Zähler nicht
    $teraz = time();
    $czasy = array_filter(json_decode(stream_get_contents($fh), true) ?: [], fn($t) => $t > $teraz - $okno);
    $ok = count($czasy) < $max;
    if ($ok) $czasy[] = $teraz;
    ftruncate($fh, 0);
    rewind($fh);
    fwrite($fh, json_encode(array_values($czasy)));
    flock($fh, LOCK_UN);
    fclose($fh);
    return $ok;
}

if (!limit_ok($_SERVER['REMOTE_ADDR'] ?? '')) {
    http_response_code(429);
    exit;
}

Steht die Website hinter einem Proxy-Server (Cloudflare, Caddy, Nginx), ist REMOTE_ADDR die Adresse dieses Servers und nicht die des Clients. Die echte Adresse müssen Sie dann aus einem Header nehmen, den Ihr Server setzt, aber nur, wenn Sie dem vertrauen, der ihn setzt. Sonst trägt der Bot eine beliebige Adresse ein und umgeht das Limit.

4. E-Mail-Validierung: die Falle, durch die das Formular Spam verschickt

Das ist der wichtigste Punkt in diesem Artikel. Ein typisches PHP-Formular sendet die Nachricht an Sie und setzt den Header Reply-To auf die Adresse der Person, die das Formular ausgefüllt hat, damit Sie mit einem Klick antworten können. Die Adresse wird meist so geprüft:

filter_var($email, FILTER_VALIDATE_EMAIL)

Das Problem: Laut Standard darf eine E-Mail-Adresse den Teil vor dem @ in Anführungszeichen setzen, und darin Zeichen mit vorangestelltem Backslash, auch ein Zeilenumbruchzeichen. Ich habe das in PHP 8.3 und 8.4 geprüft: Eine Adresse in Anführungszeichen mit Backslash und Zeilenumbruch in der Mitte besteht diese Validierung. In den Header Reply-To eingesetzt, fügt sie einen neuen Header hinzu, zum Beispiel Bcc: mit einer Liste fremder Adressen. Ihr Formular verschickt dann Spam von Ihrer Domain, über Ihren Mailserver.

Die Lösung ist einfach. Für das Formular genügen gewöhnliche Adressen, also lehnen Sie Anführungszeichen, Backslashes und Steuerzeichen ab:

function email_ok(string $e): bool
{
    return strlen($e) <= 120
        && filter_var($e, FILTER_VALIDATE_EMAIL) !== false
        && !preg_match('/[\x00-\x20\x7F"\\\\(),:;<>\[\]]/', $e);
}

Die zweite Regel: Setzen Sie in die E-Mail-Header nichts von Nutzenden ein außer der so geprüften Adresse. Betreff und Name kodieren Sie am besten (=?UTF-8?B?...?=) oder verwenden eine Bibliothek zum Mailversand, zum Beispiel PHPMailer oder Symfony Mailer, die selbst auf die Header achten. Diesen Fehler habe ich bei einem Sicherheitsaudit meiner eigenen Website gefunden und behoben, ich weiß also, dass er echten Code betrifft und nicht nur Lehrbücher.

5. Inhalt und Absenderdomain

Spam hat seine Gewohnheiten. Ein paar einfache Regeln sieben einiges von dem aus, was durch die Fallen kommt:

$linki = static fn(string $s): int => preg_match_all('~https?://|www\.~i', $s);
if ($linki($imie) > 0 || $linki($tresc) > 3) {
    odrzuc('linki');   // Link im Namen oder mehr als 3 Links im Text
}

// die Domain der E-Mail-Adresse muss E-Mails annehmen (MX- oder A-Eintrag)
$domena = substr(strrchr($email, '@') ?: '', 1);
if ($domena === '' || (!checkdnsrr($domena, 'MX') && !checkdnsrr($domena, 'A'))) {
    odrzuc('domena');
}

Die Prüfung der Domain hat noch einen Vorteil: Sie fängt Tippfehler echter Kundschaft ab, zum Beispiel gmial.com. Statt still abzulehnen, bitten Sie dann um Korrektur der Adresse, denn sonst kommt Ihre Antwort nie an.

6. Rechenaufgabe für kostspielige Formulare

Kostet das Absenden des Formulars etwas, startet es zum Beispiel eine KI, eine kostenpflichtige API oder einen großen Bericht, lohnt sich eine Rechenaufgabe (Proof-of-Work). Der Browser sucht im Hintergrund eine Zahl, die zusammen mit der erhaltenen Zeichenfolge einen SHA-256-Hash ergibt, der mit mehr als einem Dutzend Nullbits beginnt. Für einen Menschen dauert das im Hintergrund einen Bruchteil einer Sekunde, ein Bot, der tausend Einsendungen senden will, muss dagegen tausendmal mit Prozessorzeit bezahlen. So schütze ich die Tools auf meiner Website, zum Beispiel Website prüfen und die Wettbewerbsanalyse. Für ein gewöhnliches Kontaktformular ist es meist nicht nötig.

Was Sie nicht tun sollten

  • Sagen Sie dem Bot nicht, warum er abgelehnt wurde. Die Antwort „das Feld website muss leer sein“ ist eine Anleitung, die Falle zu umgehen.
  • Speichern Sie den Inhalt abgelehnter Nachrichten nicht. Zähler genügen: wie viele Ablehnungen und aus welchem Grund. Weniger Daten bedeuten weniger Pflichten aus der DSGVO.
  • Sperren Sie nicht blind Länder oder ganze Netze. Auch jemand im Urlaub oder in einer Firma mit VPN möchte schreiben.
  • Vergessen Sie die Meldungen für Menschen nicht. Greift das Limit bei einer echten Anfrage, zeigen Sie eine Telefonnummer oder E-Mail-Adresse und nicht nur einen Fehlercode.

Wann doch CAPTCHA?

Kommt trotz dieser Schutzmaßnahmen weiter Spam an, steckt meist ein Mensch oder ein Bot dahinter, der einen echten Browser steuert. Dann ist CAPTCHA sinnvoll. Wählen Sie eine Lösung, die für die meisten unsichtbar und datenschutzfreundlich ist, beschreiben Sie sie in der Datenschutzerklärung und aktivieren Sie sie nur dort, wo das Problem wirklich auftritt.

Zusammenfassung

Bot-Falle, Zeit-Ticket und Limits halten automatischen Spam ohne jede CAPTCHA auf. Eine korrekte E-Mail-Validierung sorgt dafür, dass sich das Formular nicht in einen Spam-Verteiler von Ihrer Domain verwandelt. Landen die E-Mails Ihrer Firma trotzdem bei der Kundschaft im Spam, prüfen Sie auch die Domain-Einstellungen, über die ich im Artikel SPF, DKIM und DMARC Schritt für Schritt schreibe.

Soll ich die Formulare und die Sicherheit Ihrer Website prüfen? Bestellen Sie ein Sicherheitsaudit oder schreiben Sie mir. Ich antworte innerhalb von 24 Stunden, und wer zum ersten Mal mit mir arbeitet, erhält 20 % Rabatt auf den ersten Auftrag.

Häufige Fragen

Passende Leistungen

Haben Sie ein ähnliches Projekt?

Beschreiben Sie, was Sie brauchen, und Sie erhalten innerhalb von 24 Stunden ein Angebot. Neukunden erhalten 20 % Rabatt auf den ersten Auftrag.

Angebot anfragenGespräch buchen (20 Min.)

Weiterlesen