Simple CAPTCHA

Image

To protect your forms from bots, you can require users to fill out a Simple Captcha text as seen on a dynamically created image.

Configuration

You need to explicitly enable the CAPTCHA first, and you might want to change how the CAPTCHA looks. This can be done with the captcha.set config option.

/site/config/config.php
<?php

return [
    // enable the captcha check and its API endpoints
    'bnomei.kart.captcha.enabled' => true,

    // configure how the generated image looks like
    'bnomei.kart.captcha.set' => function () {
        // https://github.com/S1SYPHOS/php-simple-captcha
        $builder = new \Bnomei\Kart\CaptchaBuilder;
        $builder->bgColor = '#FFFFFF';
        $builder->lineColor = '#FFFFFF';
        $builder->textColor = '#000000';
        $builder->applyEffects = false;
        $builder->build();
        kirby()->session()->set('captcha', $builder->phrase);

        return [
            'captcha' => $builder->inline(),
        ];
    },
    // other options...
];

Securing Forms

The plugin does provide a snippet to render the image.

<?php snippet('kart/captcha') ?>

However, the following example shows how to use Alpine.js to render the image dynamically, allowing for manual refreshes.

<form method="POST" action="<?= kart()->urls()->login() ?>">
    <label>
        <input type="email" name="email" required
                  placeholder="<?= t('email') ?>" autocomplete="email"
                  value="<?= urldecode(get('email', '')) ?>">
    </label>
    <label>
        <input type="password" name="password" required
               placeholder="<?= t('password') ?>" autocomplete="off">
    </label>
    <div x-data="{
    captcha: undefined,
    refresh() {
      fetch('<?= kart()->urls()->captcha() ?>')
        .then(response => response.json())
        .then(data => {
          this.captcha = data.captcha;
        })
    }
  }" x-init="refresh()">
        <label>
            <span>Captcha: </span>
            <input name="captcha" type="text" value=""
                   required pattern="[a-zA-Z0-9]{5}">
        </label>
        <figure>
            <img :src="captcha" width="150" height="40"/>
        </figure>
        <button x-on:click="refresh()" type="button">Refresh</button>
    </div>
    <input type="hidden" name="redirect" value="<?= $page?->url() ?>">
    <button type="submit" onclick="this.disabled=true;this.form.submit();"><?= t('login') ?></button>
</form>

Behind the Scenes

The endpoints intended for public use, which are most likely targeted by bots (login, register/signup, magic-link), are preconfigured with a check for the Simple Captcha.

If posted with the form, the \Bnomei\Kart\Router::denied()-helper will query the current request for the captcha and check whether the form is legit.

Even if you use Captcha, you should keep the CSRF-check as an additional layer of security.

If the form is successful, it will continue as intended. If not, it will redirect to the error page or return the respective HTTP status code.

Kirby Kart is not affiliated with the developers of Kirby CMS. We are merely standing on the shoulder of giants.
© 2025 Bruno Meilick All rights reserved.