Spam im Kontaktformular stoppen6 Min. Lesezeit
So fügst du ein Honeypot-Feld hinzu, das Bots ausfüllen und Menschen nicht
Füge jedem Formular einen Honeypot hinzu, der Bots abfängt, ohne Screenreader, Tastatur oder Autofill zu stören, mit der Serverprüfung für jeden Stack.
Füge ein zusätzliches Texteingabefeld hinzu, das du außerhalb des sichtbaren Bereichs versteckst, aus der Tab-Reihenfolge nimmst und in aria-hidden einpackst, mit einem Namen, den kein Browser automatisch ausfüllt. Menschen füllen es nie aus; Bots, die jedes Eingabefeld ausfüllen, schon. Behandle auf dem Server jeden Post mit einem Wert darin als Spam und antworte genauso wie auf eine echte Nachricht.
Was an einem Honeypot besonders ist
Ein Honeypot muss zwei Dinge zugleich sein. Für einen Bot, der das HTML liest, muss er wie ein normales Feld aussehen, das sich auszufüllen lohnt. Für jeden Menschen darf er nicht existieren, auch für Leute, die das Formular anders nutzen als du: jemand mit Screenreader, jemand, der mit der Tastatur tabbt, jemand, dessen Passwortmanager Formulare ausfüllt. Die meisten Honeypot-Fehler entstehen, weil eine dieser Personen vergessen wurde, und das Symptom ist eine echte Nachricht, die still als Spam abgelegt wird.
- Ein Mensch füllt das Formular mit dem aus, was er nutzt: Bildschirm, Tastatur oder Screenreader.
- Die Falle liegt außerhalb des sichtbaren Bereichs, außerhalb der Tab-Reihenfolge und ist vor Hilfstechnologien verborgen, also erreicht er sie nie.
- Ihr Name ist einer, den Browser und Passwortmanager nicht automatisch ausfüllen, also bleibt sie leer.
- Der Post kommt mit leerer Falle an und wird angenommen.
- Ein Skript-Bot parst das HTML und findet jedes Eingabefeld, auch die Falle.
- Er füllt alle aus und sendet sofort.
- Die Falle hat einen Wert, also ist der Post Spam. Der Bot bekommt trotzdem eine normale Erfolgsantwort.
Das Markup
Setze die Falle ins Formular, neben die echten Felder, als einfaches Texteingabefeld. Ein Eingabefeld mit type="hidden" funktioniert nicht: Bots wissen, dass Menschen solche Felder nicht bearbeiten können, und lassen sie in Ruhe.
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<div class="hp" aria-hidden="true">
<label for="hp-field">Leave this empty</label>
<input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
</div>
<button>Send</button>
</form><style>
.hp {
position: absolute;
left: -10000px;
width: 1px;
height: 1px;
overflow: hidden;
}
</style>aria-hidden="true"am Wrapper entfernt Label und Eingabefeld aus dem Accessibility Tree, sodass Screenreader sie nicht vorlesen.tabindex="-1"verhindert, dass die Tab-Taste auf einem Eingabefeld landet, das niemand sehen kann.- Das Label lautet Leave this empty. Falls ein Tool das Feld trotzdem anzeigt, weiß die Person, die es liest, was zu tun ist.
- Füge kein
requiredhinzu. Ein Pflichtfeld, das Menschen nicht sehen können, blockiert jedes ehrliche Absenden.
Wie du ihn versteckst
Bots haben gelernt, offensichtlich versteckte Felder zu überspringen. Einfache Scraper füllen immer noch alles aus, aber manche prüfen das hidden-Attribut oder ein Inline-display: none, bevor sie tippen. Je weiter das Verstecken vom Eingabefeld selbst entfernt ist, desto mehr Bots fallen darauf herein.
| Technik | Vor Menschen verborgen | Bots, die sie noch fängt |
|---|---|---|
hidden-Attribut oder Inline-display: none | Ja | Nur die einfachsten. Das prüft ein schlauerer Bot als Erstes |
| Off-Screen-Klasse in deinem Stylesheet | Ja | Die meisten. Der Bot müsste dein CSS laden und anwenden, um es zu merken |
opacity: 0 oder Größe null | Meistens. Manche Bildschirmlupen und Fokusrahmen zeigen es trotzdem | Die meisten, aber riskanter für Menschen |
| Per JavaScript nach dem Laden hinzugefügt | Ja | Bots, die Skripte ausführen. Einfache HTML-Scraper sehen es nie, verpassen es also |
Verwende die Off-Screen-Klasse aus deinem Stylesheet. Sie fängt die meisten Bots und ist für Menschen am sichersten. Wenn ein Honeypot nicht mehr wirkt, füge einen zweiten mit anderem Namen hinzu, statt einen raffinierteren Versteck-Trick zu suchen.
Wähle einen Namen, den Autofill ignoriert
Browser und Passwortmanager füllen Felder, indem sie anhand von name, id, Label und autocomplete-Wert raten. autocomplete="off" ist nur ein Hinweis, und manche Browser ignorieren ihn bei Feldern, die nach Adressen oder Kontaktdaten aussehen. Die eigentliche Arbeit leistet also der Name.
| Vermeiden | Warum | Stattdessen |
|---|---|---|
email2, phone, name, address | Autofill erkennt sie und füllt sie für echte Menschen aus | _gotcha, company_website |
honeypot, trap, bot | Manche Bots überspringen Felder mit offensichtlichen Namen | Ein langweiliger, plausibler Name |
url, website allein | Manche Autofill-Tools füllen ein Website-Feld aus einem gespeicherten Profil | Ein zusammengesetzter Name wie company_website |
Auf dem Server prüfen
Ein Honeypot wirkt nur, wenn der Server ihn liest. Eine Prüfung im Browser ist nutzlos, da ein Bot dein Skript nie ausführt. Hat das Feld einen Wert, antworte genauso wie auf eine echte Nachricht und speichere oder leite den Post nicht weiter. Eine andere Antwort verrät dem Bot, welches Feld ihn entlarvt hat. Die Beispiele unten rufen dein eigenes saveMessage auf; ersetze es durch das, was die Nachricht speichert oder mailt.
Ein einfaches HTML-Formular hat keinen eigenen Server. Richte es auf Ikiform aus und nenne die Falle _gotcha, oder benenne sie pro Formular unter Honeypot field um. Ikiform macht die Serverprüfung, und die Falle wird nie als Antwort gespeichert.
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<div class="hp" aria-hidden="true">
<label for="hp-field">Leave this empty</label>
<input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
</div>
<button>Send</button>
</form>Leite in einer Server Action einen in die Falle gegangenen Post auf dieselbe Dankeseite weiter wie einen echten und überspringe das Speichern.
"use server";
import { redirect } from "next/navigation";
export async function sendMessage(formData: FormData) {
if (String(formData.get("company_website") ?? "").trim() !== "") {
console.info("honeypot", { at: new Date().toISOString() });
redirect("/contact/thanks");
}
await saveMessage({
email: String(formData.get("email") ?? ""),
message: String(formData.get("message") ?? ""),
});
redirect("/contact/thanks");
}Nimm in einer Astro Action die Falle aus der Eingabe und gib das normale Ergebnis zurück, wenn sie einen Wert hat.
import { defineAction } from "astro:actions";
import { z } from "astro/zod";
export const server = {
sendMessage: defineAction({
accept: "form",
input: z.object({
email: z.email(),
message: z.string().min(1),
company_website: z.string().nullish(),
}),
handler: async ({ company_website, ...fields }) => {
if (company_website?.trim()) return { sent: true };
await saveMessage(fields);
return { sent: true };
},
}),
};Lies die Falle in .validator() und überspringe das Speichern im Handler. Beide Wege geben dieselbe Antwort zurück.
import { createServerFn } from "@tanstack/react-start";
export const sendMessage = createServerFn({ method: "POST" })
.validator((data: unknown) => {
if (!(data instanceof FormData)) throw new Error("Expected form data");
return {
email: String(data.get("email") ?? ""),
message: String(data.get("message") ?? ""),
trapped: String(data.get("company_website") ?? "").trim() !== "",
};
})
.handler(async ({ data: { trapped, ...fields } }) => {
if (!trapped) await saveMessage(fields);
return { ok: true };
});Die Route-Action leitet in beiden Fällen auf die Dankeseite weiter und speichert nur einen Post, der nicht in die Falle gegangen ist.
import { redirect } from "react-router";
import type { Route } from "./+types/contact";
export async function action({ request }: Route.ActionArgs) {
const form = await request.formData();
if (String(form.get("company_website") ?? "").trim()) return redirect("/contact/thanks");
await saveMessage({ email: String(form.get("email")), message: String(form.get("message")) });
return redirect("/contact/thanks");
}Die Form-Action speichert nur, wenn die Falle leer ist, und leitet dann in beiden Fällen mit 303 weiter.
import { redirect } from "@sveltejs/kit";
import type { Actions } from "./$types";
export const actions = {
default: async ({ request }) => {
const form = await request.formData();
if (!String(form.get("company_website") ?? "").trim()) {
await saveMessage({ email: String(form.get("email")), message: String(form.get("message")) });
}
redirect(303, "/contact/thanks");
},
} satisfies Actions;Die Server-Route antwortet auf einen Post in der Falle und einen echten mit 201 und demselben Body.
export default defineEventHandler(async (event) => {
const body = await readBody<{ email?: string; message?: string; company_website?: string }>(event);
if (body.company_website?.trim()) {
setResponseStatus(event, 201);
return { sent: true };
}
await saveMessage({ email: body.email ?? "", message: body.message ?? "" });
setResponseStatus(event, 201);
return { sent: true };
});Eine Single-Page-App kann die Falle nicht selbst prüfen. Sende sie mit den anderen Feldern und lass den Server, an den du sendest, entscheiden. FormData schließt sie ohne zusätzlichen Code ein.
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});Genauso in Vue: Behalte die Falle im <form>, damit new FormData(form) sie einschließt, und lass den Server sie prüfen.
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});Wie Ikiform mit dem Honeypot umgeht
Jedes Ikiform-Formular hat standardmäßig einen Honeypot namens _gotcha. Ändere ihn pro Formular unter Honeypot field in den Einstellungen des Formulars; jeder Name, den du wählst, ist reserviert und wird nie als Antwort gespeichert. Ein ausgefüllter Honeypot erhöht den Spam-Score um 5, was allein die Schwelle von 5 erreicht.
Ikiform verwirft den Post nicht. Es behält ihn als Spam, sodass du die Nachricht finden und wiederherstellen kannst, falls je ein echter Mensch in die Falle tappt. Ein als Spam eingestufter Post bekommt dieselbe Erfolgsantwort wie ein echter, ein Bot lernt also nichts. Er wird mit dem Status spam gespeichert, löst weder Benachrichtigungs-E-Mail noch Webhook aus, zählt nicht zu deiner Nutzung und wird nach 30 Tagen gelöscht. Jeden Montag bekommt ein Workspace, der Spam blockiert hat, eine Zusammenfassung per E-Mail mit der Gesamtzahl, den meistbetroffenen Formularen und den häufigsten Gründen. Die Spam-Dokumentation und HTML-Formulare enthalten die Referenz.
Prüfen, ob es funktioniert
- Lade das Formular mit eingeschaltetem Screenreader und geh es durch. Die Falle wird nie vorgelesen.
- Tabbe durch das Formular. Der Fokus springt vom letzten echten Feld direkt zum Button.
- Lass Browser und Passwortmanager das Formular automatisch ausfüllen. Die Falle bleibt leer.
- Sende einen Post mit ausgefülltem Honeypot,
_gotcha=bot. Er bekommt trotzdem eine Erfolgsantwort, aber die Einsendung wird als Spam gespeichert und löst weder E-Mail noch Webhook aus. - Sende eine normale Nachricht. Sie kommt als angenommen an, ohne Spur des Fallen-Felds.
Stolperfallen
- JSON-Posts. Wenn du den Body von Hand statt mit
FormDatabaust, nimm die Falle mit auf, sonst liest dein Server sie auch bei Bots als leer. - Formular-Builder, die Felder umbenennen. Manche Builder versehen Eingabefelder mit Präfixen oder benennen sie um. Prüfe im gesendeten Body, nicht im Editor, wie die Falle wirklich heißt.
- Caching. Hat eine gecachte Seite nach dem Umbenennen noch den alten Fallennamen, kommen Posts mit dem alten Namen an und gehen durch. Benenne auf beiden Seiten gleichzeitig um.
- Checkbox-Honeypots. Eine versteckte Checkbox funktioniert auch, aber Screenreader gehen mit verirrten Checkboxen schlechter um als mit Texteingabefeldern. Nimm lieber ein Texteingabefeld.
- Eine Schicht. Bots, die einen echten Browser ausführen und unsichtbare Felder überspringen, kommen an jedem Honeypot vorbei. Kombiniere ihn mit einem Rate-Limit und bei Bedarf einer Menschlichkeitsprüfung.
Fragen
Ja, gegen die häufigsten: Skripte, die jedes Eingabefeld ausfüllen, das sie finden. Bots, die einen vollständigen Browser ausführen und unsichtbare Felder überspringen, kommen durch, also nutze einen Honeypot als erste Schicht, nicht als einzige.
Nur, wenn es nachlässig versteckt ist. Pack es in ein Element mit aria-hidden, gib dem Eingabefeld tabindex -1, beschrifte es mit Leave this empty und markiere es nie als Pflicht. Screenreader und Tastatur überspringen es dann.
Meist wegen Autofill. Ein Browser oder Passwortmanager hat die Falle ausgefüllt, weil ihr Name wie ein echtes Feld aussah, etwa email2 oder phone. Benenne sie in etwas um, das kein Autofill erkennt.
Nein. Beantworte einen Post in der Falle genauso wie einen echten. Ein Fehler verrät dem Bot, welches Feld ihn erwischt hat, und die nächste Version des Bots überspringt dieses Feld.
Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.