Skip to main content
Назад до блогу
4 хв читання

Чотирирядковий спам-фільтр, що замінив reCAPTCHA на моїй контактній формі

Honeypot + time-floor + ліміти розміру. Без стороннього скрипта, без виноски про приватність, без галочки. Два місяці роботи і нуль спаму в інбоксі.

securitynext.jsux

Контактна форма на цьому сайті стріляє Telegram-сповіщенням при кожній відправці. Коли відвантажував, думав, що знадобиться reCAPTCHA — кожен гайд із захисту від спаму в інтернеті так каже. Спробував тиждень, ненавидів тертя, видрав.

Два місяці потому, без reCAPTCHA, інбокс все ще чистий. Ось патерн.

Весь фільтр, у коді

// app/api/telegram/route.ts
const data = await request.json()
const { website, mountedAt, name, email, message } = data

// 1. Honeypot — реальні користувачі ніколи не бачать це поле
if (website && website.trim().length > 0) {
  return NextResponse.json({ success: true }) // мовчки приймаємо
}

// 2. Time-floor — боти відправляють миттєво, людям треба набирати
if (typeof mountedAt === 'number' && Date.now() - mountedAt < 1500) {
  return NextResponse.json({ success: true })
}

// 3. Ліміти розміру — захищаємо downstream API від абʼюзу пейлоаду
if (name.length > 200 || email.length > 200 || message.length > 5000) {
  return NextResponse.json({ error: 'Payload too large' }, { status: 413 })
}

Ось і все. Три гарди, всі серверні. Без клієнтського скрипта.

Чому кожен важливий

Honeypot — приховане поле форми. <input name="website" tabindex="-1" autocomplete="off" style={{display:'none'}} /> на стороні React. Реальні браузери його не рендерять, реальні користувачі не заповнюють. Більшість спам-ботів обходять DOM, бачать input і старанно заповнюють кожен. У момент, коли website має значення — ти знаєш, що це бот. Критична деталь: не повертай помилку. Повертай { success: true }. Боти, що стежать за 4xx/5xx, будуть ретраїти; боти, що отримали 200, позначать форму як «готово» і підуть.

Time-floor ловить решту. Форма надсилає таймстамп маунту в прихованому полі при рендері. Коли приходить відправка, все, що менше 1.5 секунди між маунтом і відправкою — бот. Люди фізично не можуть набрати імʼя, email і повідомлення так швидко. Той самий трюк: повертаємо 200 мовчки, щоб бот не навчився відступати.

Ліміти розміру — не стільки анти-спам, скільки захист від абʼюзу. Без них нудьгуючий атакуючий міг би POST'нути 50MB тіло і розігнати квоту Telegram API. Кепай кожне поле до розумного максимуму і повертай 413 для oversize.

Чому без reCAPTCHA

reCAPTCHA вирішує реальну проблему неправильним способом для цього юзкейсу. Вона:

Для малообʼємної B2B контактної форми математика просто не працює. Вартість reCAPTCHA (втрачені легітимні відправки) вища за користь (фільтрація спаму, який honeypot і так ловить).

Коли це перестане працювати

Я б переглянув у момент, коли щось із цього стане правдою:

Для всього між — портфоліо, маленька SaaS контактна сторінка, CTA «розкажіть більше» — починай із чотирирядкової версії. Додавай складність тільки якщо реально зламається.

Повна клієнтська частина на React

Для повноти, React-сторона, що живить time-floor і honeypot:

const [mountedAt] = useState(() => Date.now())

return (
  <form onSubmit={handleSubmit}>
    {/* honeypot — візуально + семантично прихований */}
    <input
      type="text"
      name="website"
      tabIndex={-1}
      autoComplete="off"
      aria-hidden="true"
      className="absolute left-[-9999px]"
    />
    <input type="hidden" name="mountedAt" value={mountedAt} />

    {/* реальні поля */}
    <input name="name" required />
    <input name="email" type="email" required />
    <textarea name="message" required />

    <button type="submit">Надіслати</button>
  </form>
)

Три речі, які треба зробити правильно:

  1. Не використовуй display: none на honeypot. Деякі боти перевіряють computed style і пропускають. Використовуй absolute positioning за межами екрану — виглядає порожнім для людей, виглядає реальним полем для краулерів.
  2. Mount timestamp — per-render, не per-session. Якщо закешуєш у localStorage — зламаєш time-floor для легітимних користувачів, що повертаються.
  3. aria-hidden не дає скрінрідерам оголосити honeypot. Вони б і так пропустили display:none поле, але absolute-positioned inputs без aria-hidden можуть збити асистивні технології.

Урок, якщо він є: захист від спаму — це перш за все проблема UX. «Правильна» відповідь в абстракції — найважчий, найрозумніший фільтр — часто неправильна відповідь, коли врахуєш вартість, яку він накладає на легітимних користувачів. Для контактної форми на особистому сайті чотирьох рядків серверної перевірки достатньо. Капчі бережи для загроз, які реально їх заслуговують.