Замена Google reCAPTCHA на Yandex SmartCaptcha: гайд по 152-ФЗ

Веб Штурм
  • 152-ФЗ
  • SmartCaptcha
  • формы
  • безопасность
  • PSI

С 1 июля 2025 использование Google reCAPTCHA на формах сбора персональных данных в РФ — это нарушение 152-ФЗ из-за трансграничной передачи данных. Штрафы по типовым нарушениям — от 300 000 ₽ за отсутствие регистрации оператором до 15-18 млн ₽ за повторные/массовые. У нас на ozsm.ru закрыли это за один спринт (B13, 2 мая 2026): SmartCaptcha + honeypot + time-trap. Заодно — PSI Mobile на /kontakt поднялся с 89 до 100, TBT обнулился, спам прекратился полностью.

Контекст

Что именно нарушает reCAPTCHA по 152-ФЗ:

Реальные штрафы 2025-2026 (vc.ru, klerk.ru):

Альтернативы и почему SmartCaptcha

В России доступно три заметных решения:

Выбрали SmartCaptcha по доступности (бесплатная для разумных объёмов), документации и работающему фоновому режиму.

B13-паттерн: lazy-load на first interaction

Главный анти-паттерн при подключении любой капчи — грузить её скрипт сразу при загрузке страницы. Это убивает PSI: на /kontakt ozsm.ru до миграции лежало ~363 КБ служебного кода Google, TBT 1648 мс.

Lazy-load по first user interaction решает обе проблемы — и юридическую (уходит трансграничный код), и performance.

<!-- На странице — placeholder без подключения скрипта -->
<form id="contact-form" data-needs-captcha>
  <input type="text" name="name" required>
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>

  <!-- honeypot — невидимое поле-ловушка -->
  <input type="text" name="website"
         tabindex="-1"
         aria-hidden="true"
         autocomplete="off"
         style="position:absolute;left:-9999px;opacity:0">

  <!-- time-trap — отметка времени монтирования -->
  <input type="hidden" name="form_ts" value="">

  <div id="captcha-container"></div>
  <button type="submit">Отправить</button>
</form>

<script type="module" src="/smartcaptcha-loader.js" defer></script>
// /public/smartcaptcha-loader.js
// Грузит smartcaptcha.js только при первом взаимодействии или через 8с
let loaded = false;
const trigger = () => {
  if (loaded) return;
  loaded = true;
  const s = document.createElement('script');
  s.src = 'https://smartcaptcha.yandexcloud.net/captcha.js?render=onload&onload=__sc_init__';
  s.defer = true;
  document.head.appendChild(s);
};

window.__sc_init__ = () => {
  document.dispatchEvent(new CustomEvent('smartcaptcha:ready'));
};

// Триггеры — любое взаимодействие с формой ИЛИ 8 секунд
const events = ['focusin', 'input', 'click', 'touchstart'];
const off = events.map(e =>
  document.addEventListener(e, trigger, { once: true, passive: true })
);
setTimeout(trigger, 8000);

// Заполняем form_ts при загрузке
document.querySelectorAll('input[name="form_ts"]').forEach(el => {
  el.value = String(Date.now());
});

Server-side проверки: honeypot + time-trap + captcha verify

Клиент подделать тривиально. Все три проверки происходят на сервере — клиент не отвечает за безопасность.

// app/src/lib/contact-handler.ts
export interface ContactValidationDeps {
  verifyCaptcha: (token: string) => Promise<boolean>;
  sendEmail: (data: ContactData) => Promise<void>;
}

export async function validateAndProcessContact(
  formData: FormData,
  deps: ContactValidationDeps
): Promise<{ ok: true } | { ok: false; code: string }> {
  // 1. honeypot — заполнено ботом
  if (formData.get('website')) {
    return { ok: false, code: 'honeypot' };
  }

  // 2. time-trap — форма отправлена быстрее, чем за 3с
  const ts = Number(formData.get('form_ts') ?? 0);
  if (Date.now() - ts < 3000) {
    return { ok: false, code: 'too_fast' };
  }

  // 3. captcha — реальная проверка через Yandex API
  const token = String(formData.get('smart-token') ?? '');
  if (!await deps.verifyCaptcha(token)) {
    return { ok: false, code: 'captcha' };
  }

  // 4. реальная обработка
  await deps.sendEmail({
    name: String(formData.get('name')),
    email: String(formData.get('email')),
    message: String(formData.get('message')),
  });
  return { ok: true };
}

Реальная имплементация verifyCaptcha:

async function verifyCaptcha(token: string): Promise<boolean> {
  const res = await fetch('https://smartcaptcha.yandexcloud.net/validate', {
    method: 'POST',
    body: new URLSearchParams({
      secret: process.env.SMARTCAPTCHA_SERVER_KEY!,
      token,
      ip: '0.0.0.0', // если знаем клиентский IP — передаём
    }),
  });
  const data = await res.json();
  return data.status === 'ok';
}

Результаты на ozsm.ru

Метрика на /kontaktДо миграцииПосле B13
PSI Mobile Performance89100
TBT1 648 мс0 мс
Размер JS на странице~363 КБ Google~12 КБ Яндекс (lazy)
Спам через форму (за неделю)50-800
Юр.риск штрафа 152-ФЗ1-6 млн ₽закрыт

Спринт занял два дня. Из них одна задача — регистрация sitekey в Yandex Cloud (5 минут), остальное — паттерн lazy-load и server-side проверки.

Что мы из этого вынесли

  1. 152-ФЗ — это не косметика, а юридический риск 1-18 млн ₽. Сайт с reCAPTCHA после 1 июля 2025 — мина под бюджет. Замена занимает 1-2 дня; штраф — годовой бюджет на маркетинг.
  2. Защищать форму нужно тремя слоями. SmartCaptcha + honeypot + time-trap. Любой один слой пропустит 5-10% ботов; три вместе — около 0.
  3. Lazy-load капчи бесплатно даёт +10-20 PSI. До first interaction скрипт капчи никому не нужен — не грузите его.

Ссылки