Защита формы на PHP: honeypot и rate limit без фреймворков
Форма обратной связи на PHP кажется простой задачей: имя, телефон и отправка письма на почту. Но как только сайт становится публичным, она быстро превращается в точку атаки для ботов и спам-скриптов.
Эта статья пошагово показывает, как превратить обычную форму в защищённый механизм приёма заявок без фреймворков - только PHP, HTML и немного JavaScript. Подходит для начинающих и разработчиков с базовым уровнем.
Защита формы - это набор техник, которые помогают отличать живого пользователя от автоматического бота и предотвращать спам-отправки.
Обычно базовая форма без защиты уязвима сразу по нескольким направлениям: её можно отправлять напрямую через POST-запросы, перегружать массовыми запросами и подсовывать мусорные данные.
Базовая форма и первая проблема
Начнём с обычной HTML-формы:
<form id="lead-form">
<input name="name" placeholder="Имя" required>
<input name="phone" placeholder="Телефон" required>
<button type="submit">Отправить</button>
</form>С точки зрения браузера всё корректно, но серверу нельзя доверять тому, что приходит из формы. Любой бот может отправить POST-запрос напрямую, вообще не открывая сайт.
Honeypot как первая линия защиты
Один из самых простых способов отсеять ботов - добавить скрытое поле. Пользователь его не видит, но многие боты заполняют все доступные input-поля автоматически.
<div style="position:absolute;left:-9999px;">
<input name="hp_token" autocomplete="off" tabindex="-1">
</div>На сервере проверка выглядит максимально просто: если поле заполнено, запрос можно игнорировать.
$honeypot = trim($_POST['hp_token'] ?? '');
if ($honeypot !== '') {
exit('OK');
}Здесь важно не возвращать ошибку. Бот не должен понимать, что его отфильтровали. Для него это выглядит как обычная успешная отправка.
После внедрения такого поля количество простого спама обычно резко падает, потому что автоматические скрипты не анализируют интерфейс, а просто заполняют всё подряд.
Ограничение частоты запросов
Следующая проблема появляется почти сразу: даже если часть ботов отсеялась, остаются скрипты, которые отправляют запросы с разными параметрами.
Чтобы с этим справиться, добавляется ограничение по IP. Логика здесь простая - один пользователь не должен отправлять форму слишком часто.
Обычно используется временное окно. Например, пять заявок за десять минут.
function is_rate_limited($ip)
{
$file = __DIR__ . '/rate.json';
$data = file_exists($file)
? json_decode(file_get_contents($file), true)
: [];
$now = time();
$hits = $data[$ip] ?? [];
$hits = array_filter($hits, function ($t) use ($now) {
return ($now - $t) < 600;
});
return count($hits) >= 5;
}После успешной отправки заявки фиксируется время запроса:
function record_hit($ip)
{
$file = __DIR__ . '/rate.json';
$data = file_exists($file)
? json_decode(file_get_contents($file), true)
: [];
$data[$ip][] = time();
file_put_contents($file, json_encode($data));
}Такой подход не идеален, но уже эффективно снижает нагрузку и режет массовые атаки.
Проверка данных на сервере
Даже если форма выглядит корректной в браузере, сервер обязан проверять каждое поле самостоятельно.
Имя должно быть адекватной длины и содержать только буквы и пробелы:
function valid_name($name)
{
$name = trim($name);
return mb_strlen($name) >= 2
&& preg_match('/^[\p{L}\s\-]+$/u', $name);
}Телефон лучше очищать от всего лишнего и проверять по длине:
function valid_phone($phone)
{
$digits = preg_replace('/\D+/', '', $phone);
return preg_match('/^\d{10,11}$/', $digits);
}Этот этап часто недооценивают, хотя именно он защищает от мусорных данных и потенциальных атак через форму.
Отправка письма
Когда данные прошли все проверки, можно формировать письмо.
$subject = mb_encode_mimeheader('Новая заявка', 'UTF-8');
$headers = "Content-Type: text/html; charset=UTF-8\r\n";
$headers .= "From: site@test.ru\r\n";
mail('admin@test.ru', $subject, $html, $headers);Важно понимать, что mail() не гарантирует доставку. Он лишь передаёт письмо почтовому серверу, а дальше уже работают фильтры спама и политика доставки.
HTML-шаблон письма
Чтобы письмо было читаемым, лучше сразу использовать простую HTML-разметку:
$html = "
<div style='font-family:Arial'>
<h2>Новая заявка</h2>
<p>Имя: $name</p>
<p>Телефон: <a href='tel:$phone'>$phone</a></p>
</div>";Такой формат нормально отображается в большинстве почтовых клиентов и удобен для менеджеров.
Обработка отправки на фронтенде
На стороне JavaScript важно не просто отправлять форму, а обрабатывать ответ сервера:
fetch('/send.php', {
method: 'POST',
body: new FormData(document.querySelector('#lead-form'))
})
.then(res => res.text())
.then(response => {
if (response === 'OK') {
alert('Заявка отправлена');
} else {
alert('Ошибка отправки');
}
});Это позволяет контролировать реальный результат, а не только факт HTTP-запроса.
Резервное сохранение заявок
Практика показывает, что почта не всегда работает стабильно. Поэтому полезно сохранять заявки локально.
file_put_contents(
__DIR__ . '/leads.txt',
date('Y-m-d H:i:s') . " | $name | $phone\n",
FILE_APPEND
);Это простая страховка, которая помогает не терять заявки даже при проблемах с почтовым сервером.
Итоговая логика работы
Вся система защиты складывается в цепочку, где каждый этап фильтрует свой тип проблем: скрытое поле отсеивает простых ботов, ограничение запросов снижает нагрузку, валидация защищает данные, а резервное сохранение страхует от потерь.
В итоге форма перестаёт быть открытой точкой входа и начинает работать как контролируемый канал заявок.
Комментарии
Чтобы оставить комментарий, войдите в аккаунт.