mulertech / captcha
Symfony bundle providing a self-hosted image-based math captcha for bot protection
Requires
- php: ^8.4
- ext-gd: *
- symfony/dependency-injection: ^6.4 || ^7.0 || ^8.0
- symfony/form: ^6.4 || ^7.0 || ^8.0
- symfony/http-foundation: ^6.4 || ^7.0 || ^8.0
- symfony/http-kernel: ^6.4 || ^7.0 || ^8.0
- symfony/routing: ^6.4 || ^7.0 || ^8.0
- symfony/validator: ^6.4 || ^7.0 || ^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- mulertech/docker-dev: ^3.2
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.5 || ^12.0
- symfony/config: ^6.4 || ^7.0 || ^8.0
- symfony/yaml: ^6.4 || ^7.0 || ^8.0
- twig/twig: ^3.0 || ^4.0
Suggests
- twig/twig: Required to use the bundled form theme
README
A self-hosted, image-based math captcha Symfony bundle. No external service required. Generates a random arithmetic operation rendered as a low-quality JPEG with noise, validated server-side via session. Protects contact and quote forms against bots without any third-party dependency.
Requirements
- PHP 8.4+
- ext-gd
- Symfony 6.4, 7.x or 8.x
Installation
composer require mulertech/captcha
Routes and form theme are automatically registered — no additional configuration required.
The bundle registers two endpoints:
| Route | Path | Description |
|---|---|---|
mulertech_captcha_image |
GET /captcha/image?token=xxx |
Returns the JPEG captcha image |
mulertech_captcha_refresh |
GET /captcha/refresh |
Returns {token, imageUrl} JSON for JS refresh |
Usage
1. Add CaptchaType to your form
use MulerTech\CaptchaBundle\Form\CaptchaType; $builder->add('captcha', CaptchaType::class);
The field is automatically mapped: false and includes the ValidCaptcha constraint.
2. Render in your Twig template
Render the field before the submit button. The widget displays the captcha image, a refresh button (JavaScript, no page reload), and the answer input:
{{ form_row(form.captcha) }}
<button type="submit">Envoyer</button>
{{ form_end(form) }}
3. Process the form in your controller
No special handling required. $form->isValid() returns false if the captcha answer is wrong
or expired, with a localized error message on the captcha field.
$form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // captcha passed — process the form }
Error messages
| Situation | Message |
|---|---|
| Wrong answer | Le code de vérification est incorrect. |
| Token expired / missing | Le code de vérification a expiré, veuillez recommencer. |
CSP nonce support
The bundle's inline <script> tag supports Content Security Policy nonces.
Automatic (with mulertech/csp-bundle)
If mulertech/csp-bundle is installed, the
nonce is injected automatically — no additional configuration needed.
Manual
Pass the nonce explicitly via the csp_nonce option:
$builder->add('captcha', CaptchaType::class, [ 'csp_nonce' => $this->cspNonceGenerator->getNonce('main'), ]);
The nonce is added to the <script> tag rendered by the form theme:
<script nonce="abc123">...</script>
Security considerations
- Token TTL: captcha tokens expire after 10 minutes.
- Session limit: a maximum of 5 active tokens per session prevents session flooding.
- Answer space: math operations produce answers in the range 1–30. Apply rate limiting at the application level (e.g., Symfony's RateLimiter) to prevent brute-force attempts.
Testing
./vendor/bin/mtdocker test-ai