Configuring Anti-Spam Protection for 1C-Bitrix Forms
Spam in forms is not merely an annoyance — it is technically costly: it pollutes the CRM with junk leads, overloads email queues, and triggers false positives in analytics. The standard Bitrix CAPTCHA (bitrix:main.captcha) — an image with symbols — has been virtually ineffective since 2020: modern bots recognise it with 95%+ accuracy. A multi-layered approach is required.
Layers of protection
Effective anti-spam protection consists of several independent layers, each filtering a different type of attack:
- Honeypot — a hidden field that only bots fill in
- Timing — a form submitted faster than 3 seconds after loading is a bot
- Rate limiting — restricting the number of submissions from a single IP
- reCAPTCHA v3 / Turnstile — JS-based verification without CAPTCHA images
- Email validation — MX record check, disposable email domains
- Phone validation — valid format, not on the blocklist
Honeypot: the simplest and most effective method
// In the form template — a hidden field, invisible via CSS (not display:none — that is suspicious to some bots)
?>
<div style="position:absolute;left:-9999px;top:-9999px;opacity:0;height:0;overflow:hidden">
<label for="email_confirm">Leave empty</label>
<input type="text"
id="email_confirm"
name="email_confirm"
tabindex="-1"
autocomplete="off"
value="">
</div>
<?php
Server-side check:
// If the field is filled — it's a bot
if (!empty($data['email_confirm'])) {
// Silently ignore — do not show an error to the bot
echo json_encode(['success' => true]);
exit;
}
Timing: the form cannot be submitted instantly
// When rendering the form, write a timestamp to a hidden field
$formToken = base64_encode(json_encode([
'ts' => time(),
'sessid' => bitrix_sessid(),
]));
<input type="hidden" name="form_token" value="<?= htmlspecialchars($formToken) ?>">
Server-side check:
$token = json_decode(base64_decode($data['form_token'] ?? ''), true);
$timeElapsed = time() - (int)($token['ts'] ?? 0);
if ($timeElapsed < 3) {
// Too fast — bot indicator
$this->markSpam('timing', $timeElapsed);
exit(json_encode(['success' => true])); // silent success for the bot
}
if ($timeElapsed > 3600) {
// Form has expired — return an error to the user
exit(json_encode(['success' => false, 'error' => 'Session expired. Please refresh the page.']));
}
Rate Limiter via Redis / Bitrix Cache
namespace Local\AntiSpam;
class RateLimiter
{
private const LIMITS = [
'ip' => ['max' => 5, 'window' => 3600], // 5 submissions per IP per hour
'email' => ['max' => 3, 'window' => 86400], // 3 submissions per email per day
'phone' => ['max' => 2, 'window' => 86400], // 2 submissions per phone per day
];
public function check(string $type, string $identifier): bool
{
$limit = self::LIMITS[$type] ?? ['max' => 3, 'window' => 3600];
$key = 'spam_rl_' . $type . '_' . md5($identifier);
$cache = \Bitrix\Main\Application::getInstance()->getManagedCache();
$count = (int)$cache->get($key);
if ($count >= $limit['max']) {
$this->logAttempt($type, $identifier, 'rate_limit');
return false;
}
$cache->set($key, $count + 1, $limit['window']);
return true;
}
private function logAttempt(string $type, string $identifier, string $reason): void
{
\CEventLog::Add([
'SEVERITY' => 'WARNING',
'AUDIT_TYPE_ID' => 'SPAM_BLOCKED',
'MODULE_ID' => 'local.antispam',
'DESCRIPTION' => "Blocked: type={$type}, id={$identifier}, reason={$reason}",
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'],
]);
}
}
reCAPTCHA v3: scoring without UX interruption
Google reCAPTCHA v3 presents no CAPTCHA challenge to the user — it runs in the background and returns a score from 0 to 1 (0 = bot, 1 = human).
Integrating into the form:
// Load reCAPTCHA v3
// <script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
async function submitForm(formData) {
const token = await grecaptcha.execute('YOUR_SITE_KEY', { action: 'callback_form' });
formData.recaptcha_token = token;
formData.recaptcha_action = 'callback_form';
// submit...
}
Server-side verification:
namespace Local\AntiSpam;
class RecaptchaV3Verifier
{
private string $secretKey;
private float $minScore;
public function __construct(float $minScore = 0.5)
{
$this->secretKey = \Bitrix\Main\Config\Option::get('local.antispam', 'recaptcha_secret');
$this->minScore = $minScore;
}
public function verify(string $token, string $expectedAction = ''): bool
{
$ch = curl_init('https://www.google.com/recaptcha/api/siteverify');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'secret' => $this->secretKey,
'response' => $token,
'remoteip' => $_SERVER['REMOTE_ADDR'],
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!($response['success'] ?? false)) {
return false;
}
// Verify action
if ($expectedAction && ($response['action'] ?? '') !== $expectedAction) {
return false;
}
return ($response['score'] ?? 0) >= $this->minScore;
}
}
For audiences where Google reCAPTCHA may be blocked, the alternatives are Cloudflare Turnstile (no VPN issues) or Yandex SmartCaptcha.
Yandex SmartCaptcha: an alternative option
// Include
// <script src="https://captcha-api.yandex.ru/captcha.js" defer></script>
// Render the widget
const captchaId = window.smartCaptcha.render('captcha-container', {
sitekey : 'YOUR_SITE_KEY',
callback : (token) => { document.getElementById('smart-token').value = token; },
});
Server-side verification:
function verifyYandexCaptcha(string $token): bool
{
$secretKey = \Bitrix\Main\Config\Option::get('local.antispam', 'yandex_captcha_secret');
$ch = curl_init('https://smartcaptcha.yandexcloud.net/validate');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'secret' => $secretKey,
'token' => $token,
'ip' => $_SERVER['REMOTE_ADDR'],
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
return ($result['status'] ?? '') === 'ok';
}
Disposable email validation
Temporary email addresses (mailinator.com, guerrillamail.com, etc.) are a spam indicator:
class EmailValidator
{
private const DISPOSABLE_DOMAINS_URL =
'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf';
public function isDisposable(string $email): bool
{
$domain = strtolower(substr(strrchr($email, '@'), 1));
// Cache the list for 24 hours
$cache = \Bitrix\Main\Data\Cache::createInstance();
if (!$cache->initCache(86400, 'disposable_domains', '/antispam/')) {
$cache->startDataCache();
$list = file(self::DISPOSABLE_DOMAINS_URL, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$cache->endDataCache($list);
} else {
$list = $cache->getVars();
}
return in_array($domain, $list ?? [], true);
}
}
Scope of work
- Honeypot implementation across all site forms
- Timing validation via hidden timestamp token
- Rate limiter by IP, email, and phone
- reCAPTCHA v3 or Yandex SmartCaptcha integration
- Disposable email check at registration and subscription
- Logging blocked attempts to
CEventLog - Statistics dashboard: number of blocked attempts by type over a period
Timeline: basic set (honeypot + timing + rate limit) — 3–5 days. Full stack with SmartCaptcha and monitoring — 1–2 weeks.







