Hentikan spam formulir kontak5 menit baca
Cara menambahkan field honeypot yang diisi bot tetapi tidak oleh manusia
Tambahkan honeypot ke formulir apa pun yang menangkap bot tanpa mengganggu screen reader, keyboard, atau autofill, dengan pemeriksaan server untuk setiap stack.
Tambahkan satu input teks ekstra yang Anda sembunyikan di luar layar, keluarkan dari urutan tab, dan bungkus dengan aria-hidden, dengan nama yang tidak diisi otomatis oleh browser mana pun. Manusia tidak pernah mengisinya; bot yang mengisi setiap input akan mengisinya. Di server, anggap setiap kiriman yang field itu berisi nilai sebagai spam, dan jawab persis seperti untuk pesan asli.
Apa yang berbeda dari honeypot
Honeypot harus menjadi dua hal sekaligus. Bagi bot yang membaca HTML, ia harus terlihat seperti field biasa yang layak diisi. Bagi setiap orang, ia harus tidak ada, termasuk orang yang memakai formulir dengan cara berbeda dari Anda: pengguna screen reader, orang yang menavigasi dengan keyboard, orang yang formulirnya diisi oleh password manager. Sebagian besar bug honeypot datang dari melupakan salah satu orang ini, dan gejalanya adalah pesan asli yang diam-diam masuk ke spam.
- Seseorang mengisi formulir dengan apa pun yang ia pakai: layar, keyboard, atau screen reader.
- Jebakan ada di luar layar, di luar urutan tab, dan tersembunyi dari teknologi bantu, jadi ia tidak pernah mencapainya.
- Namanya tidak diisi otomatis oleh browser dan password manager, jadi tetap kosong.
- Kiriman tiba dengan jebakan kosong dan diterima.
- Bot skrip mem-parse HTML dan menemukan setiap input, termasuk jebakannya.
- Bot mengisi semuanya dan langsung mengirim.
- Jebakan berisi nilai, jadi kiriman dianggap spam. Bot tetap mendapat jawaban sukses biasa.
Markup-nya
Taruh jebakan di dalam formulir, di samping field asli, sebagai input teks biasa. Input type="hidden" tidak berhasil: bot tahu orang tidak bisa mengeditnya, dan membiarkannya.
<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"pada wrapper menghapus label dan input dari accessibility tree, sehingga screen reader tidak membacakannya.tabindex="-1"mencegah tombol Tab berhenti di input yang tidak terlihat siapa pun.- Labelnya berbunyi Leave this empty (biarkan kosong). Jika ada alat yang masih menampilkan field itu, orang yang membacanya tahu apa yang harus dilakukan.
- Jangan tambahkan
required. Field wajib yang tidak bisa dilihat orang memblokir setiap kiriman yang jujur.
Cara menyembunyikannya
Bot sudah belajar melewati field yang jelas-jelas tersembunyi. Scraper sederhana masih mengisi semuanya, tetapi sebagian memeriksa atribut hidden atau display: none inline sebelum mengetik. Semakin jauh penyembunyiannya dari input itu sendiri, semakin banyak bot yang terjebak.
| Teknik | Tersembunyi dari manusia | Bot yang masih tertangkap |
|---|---|---|
Atribut hidden atau display: none inline | Ya | Hanya yang paling sederhana. Ini hal pertama yang diperiksa bot yang lebih pintar |
| Class di luar layar di stylesheet Anda | Ya | Sebagian besar. Bot harus memuat dan menerapkan CSS Anda untuk mengetahuinya |
opacity: 0 atau ukuran nol | Sebagian besar. Beberapa pembesar layar dan outline fokus masih menampakkannya | Sebagian besar, tetapi lebih berisiko bagi manusia |
| Ditambahkan JavaScript setelah halaman dimuat | Ya | Bot yang menjalankan skrip. Scraper HTML biasa tidak pernah melihatnya, jadi mereka lolos |
Pakai class di luar layar dari stylesheet Anda. Cara ini menangkap bot paling banyak dan paling aman bagi manusia. Jika satu honeypot berhenti bekerja, tambahkan yang kedua dengan nama berbeda, alih-alih trik penyembunyian yang lebih cerdik.
Pilih nama yang diabaikan autofill
Browser dan password manager mengisi field dengan menebak dari name, id, label, dan nilai autocomplete-nya. autocomplete="off" hanyalah petunjuk, dan sebagian browser mengabaikannya untuk field yang terlihat seperti alamat atau detail kontak. Jadi nama yang sebenarnya berperan.
| Hindari | Alasan | Gunakan sebagai gantinya |
|---|---|---|
email2, phone, name, address | Autofill mengenalinya dan mengisinya untuk orang sungguhan | _gotcha, company_website |
honeypot, trap, bot | Sebagian bot melewati field dengan nama yang mencolok | Nama yang membosankan tetapi masuk akal |
url, website saja | Sebagian alat autofill mengisi field website dari profil tersimpan | Nama gabungan seperti company_website |
Periksa di server
Honeypot hanya berfungsi jika server membacanya. Pemeriksaan di sisi browser tidak berguna, karena bot tidak pernah menjalankan skrip Anda. Saat field berisi nilai, jawab persis seperti untuk pesan asli, dan jangan simpan atau teruskan kirimannya. Jawaban yang berbeda memberi tahu bot field mana yang membongkarnya. Contoh di bawah memanggil saveMessage milik Anda sendiri; ganti dengan apa pun yang menyimpan atau mengirim email pesan itu.
Formulir HTML biasa tidak punya server sendiri. Arahkan ke Ikiform dan beri nama jebakannya _gotcha, atau ganti namanya per formulir di Honeypot field. Ikiform melakukan pemeriksaan server, dan jebakan tidak pernah disimpan sebagai jawaban.
<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>Di server action, redirect kiriman yang terjebak ke halaman terima kasih yang sama dengan kiriman asli, dan lewati penyimpanannya.
"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");
}Di Astro Action, keluarkan jebakan dari input dan kembalikan hasil normal saat jebakan berisi nilai.
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 };
},
}),
};Baca jebakan di .validator() dan lewati penyimpanan di handler. Kedua jalur mengembalikan jawaban yang sama.
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 };
});Route action me-redirect ke halaman terima kasih dalam kedua kasus dan hanya menyimpan kiriman yang tidak terjebak.
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");
}Form action hanya menyimpan saat jebakan kosong, lalu me-redirect dengan 303 untuk keduanya.
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;Server route menjawab 201 dengan body yang sama untuk kiriman yang terjebak maupun yang asli.
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 };
});Single-page app tidak bisa memeriksa jebakan sendiri. Kirim bersama field lainnya, dan biarkan server tujuan yang memutuskan. FormData sudah menyertakannya tanpa kode tambahan.
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});Sama di Vue: biarkan jebakan di dalam <form> agar new FormData(form) menyertakannya, dan biarkan server yang memeriksanya.
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});Cara Ikiform menangani honeypot
Setiap formulir Ikiform punya honeypot bernama _gotcha secara default. Ganti per formulir di Honeypot field pada pengaturan formulir; nama apa pun yang Anda pilih dicadangkan dan tidak pernah disimpan sebagai jawaban. Honeypot yang terisi menambah 5 ke skor spam, yang sudah mencapai ambang 5 dengan sendirinya.
Ikiform tidak membuang kirimannya. Kiriman disimpan sebagai spam, jadi jika suatu saat orang sungguhan terkena jebakan, Anda bisa menemukan pesannya dan memulihkannya. Kiriman yang dinilai spam mendapat jawaban sukses yang sama dengan kiriman asli, jadi bot tidak belajar apa-apa. Kiriman itu disimpan dengan status spam, tidak mengirim email notifikasi maupun webhook, tidak dihitung dalam pemakaian Anda, dan dihapus setelah 30 hari. Setiap Senin, workspace yang memblokir spam menerima satu email ringkasan berisi total, formulir tersibuk, dan alasan teratas. Dokumentasi spam dan formulir HTML berisi referensinya.
Pastikan berfungsi
- Muat formulir dengan screen reader aktif dan telusuri. Jebakan tidak pernah dibacakan.
- Tekan Tab di sepanjang formulir. Fokus berpindah dari field asli terakhir langsung ke tombol.
- Biarkan browser dan password manager mengisi formulir otomatis. Jebakan tetap kosong.
- Kirim dengan honeypot terisi,
_gotcha=bot. Kiriman tetap mendapat jawaban sukses, tetapi disimpan sebagai spam dan tidak memicu email atau webhook. - Kirim pesan biasa. Pesan masuk sebagai diterima, tanpa jejak field jebakan.
Hal yang sering menjebak
- Kiriman JSON. Jika Anda menyusun body secara manual alih-alih dengan
FormData, sertakan jebakannya, atau server Anda akan membacanya kosong untuk bot juga. - Pembuat formulir yang mengganti nama field. Sebagian pembuat situs memberi awalan atau mengganti nama input. Cek body yang dikirim, bukan editornya, untuk mengetahui nama asli jebakan.
- Cache. Jika halaman yang di-cache masih memakai nama jebakan lama setelah Anda menggantinya, kiriman datang dengan nama lama dan lolos. Ganti nama di kedua sisi sekaligus.
- Honeypot checkbox. Checkbox tersembunyi juga bisa, tetapi screen reader menangani checkbox nyasar lebih buruk daripada input teks. Utamakan input teks.
- Hanya satu lapisan. Bot yang menjalankan browser sungguhan dan melewati field tak terlihat bisa lolos dari honeypot apa pun. Pasangkan dengan rate limit dan, jika perlu, pemeriksaan manusia.
Pertanyaan
Ya, melawan yang paling umum: skrip yang mengisi setiap input yang ditemukannya. Bot yang menjalankan browser penuh dan melewati field tak terlihat bisa lolos, jadi pakai honeypot sebagai lapisan pertama, bukan satu-satunya.
Hanya jika disembunyikan sembarangan. Bungkus dalam elemen dengan aria-hidden, beri input tabindex -1, beri label Leave this empty, dan jangan pernah menandainya wajib. Screen reader dan keyboard pun akan melewatinya.
Biasanya karena autofill. Browser atau password manager mengisi jebakan karena namanya terlihat seperti field asli, misalnya email2 atau phone. Ganti namanya menjadi sesuatu yang tidak dikenali autofill.
Tidak. Jawab kiriman yang terjebak persis seperti kiriman asli. Error memberi tahu bot field mana yang menangkapnya, dan versi bot berikutnya akan melewati field itu.
Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.