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 ловит остальное. Форма отправляет timestamp маунта в скрытом поле при рендере. Когда приходит отправка, всё, что меньше 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. «Правильный» ответ в абстракции — самый тяжёлый, самый умный фильтр — часто неправильный ответ, когда учтёшь стоимость, которую он накладывает на легитимных пользователей. Для контактной формы на личном сайте четырёх строк серверной проверки хватает. Капчи береги для угроз, которые реально их заслуживают.