Setting up anti-spam protection for 1C-Bitrix forms

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

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:

  1. Honeypot — a hidden field that only bots fill in
  2. Timing — a form submitted faster than 3 seconds after loading is a bot
  3. Rate limiting — restricting the number of submissions from a single IP
  4. reCAPTCHA v3 / Turnstile — JS-based verification without CAPTCHA images
  5. Email validation — MX record check, disposable email domains
  6. 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.