dapphp/securimage
Securimage is a PHP CAPTCHA library that generates customizable captcha images and audio challenges to protect forms from spam. It supports custom fonts, colors, backgrounds, wordlists, and audio captchas, and integrates easily into existing PHP applications and frameworks.
Securimage is a plain-PHP CAPTCHA library, not a Laravel package. There is no service provider, facade, config publish, or auto-discovery. You wire it up yourself.
1. Install and verify requirements
composer require dapphp/securimage
Then verify:
gd_info()['FreeType Support'] must be true.$_SESSION) or a PDO database, depending on the storage mode you choose.Confirm how the class is autoloaded:
// Usually a global class, not namespaced
$captcha = new \Securimage();
Check vendor/composer/autoload_classmap.php if unsure; older releases often use classmap autoloading rather than PSR-4.
2. Publish/reference assets
The package ships fonts and audio files inside vendor/dapphp/securimage/. In Laravel, the safest approach is to reference absolute vendor paths from config rather than trying to asset() them:
// config/securimage.php
return [
'font_file' => base_path('vendor/dapphp/securimage/fonts/DejaVuSans.ttf'),
'audio_path' => base_path('vendor/dapphp/securimage/audio'),
'image_width' => 215,
'image_height' => 80,
'code_length' => 6,
'expiry_time' => 900,
'charset' => 'ABCDEFGHKLMNPRSTUVWYZabcdefghklmnprstuvwyz23456789',
];
If you copy assets into storage/ or public/, use absolute paths in config; Securimage does not resolve Laravel paths for you.
3. Add routes and a controller
// routes/web.php
Route::get('/captcha/image', [CaptchaController::class, 'image'])
->name('captcha.image')
->middleware('throttle:60,1');
Route::get('/captcha/audio', [CaptchaController::class, 'audio'])
->name('captcha.audio')
->middleware('throttle:30,1');
A minimal image action:
public function image()
{
$captcha = new \Securimage(config('securimage'));
ob_start();
$captcha->show();
$body = ob_get_clean();
return response($body, 200, [
'Content-Type' => $captcha->getMimeType(),
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
'Pragma' => 'no-cache',
]);
}
If your version of show() calls exit, you cannot buffer it inside a normal Laravel response. In that case, either use a dedicated route that ends with exit, or switch to a method/API that returns the image bytes.
4. Put it on a form
<form method="POST" action="{{ route('register') }}">
@csrf
<img src="{{ route('captcha.image') }}?t={{ now()->timestamp }}"
alt="CAPTCHA image"
id="captcha-image">
<button type="button" onclick="document.getElementById('captcha-image').src='{{ route('captcha.image') }}?t=' + Date.now()">
Refresh
</button>
<input type="text" name="captcha_code" autocomplete="off" required>
@error('captcha_code')
<span>{{ $message }}</span>
@enderror
</form>
Where to look first
vendor/dapphp/securimage/examples/ contains the canonical securimage_show.php and securimage_play.php scripts. Treat those as the reference implementation.securimage.php documents the full option array.First real use case: protect registration, login, password reset, or a public contact form. Start with the image endpoint plus a Laravel validation rule, then add audio for accessibility.
This is the cleanest day-to-day integration. Encapsulate Securimage validation so controllers and form requests stay framework-idiomatic.
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Securimage;
class ValidCaptcha implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! config('securimage.enabled', true)) {
return;
}
$captcha = new Securimage(config('securimage'));
if (! $captcha->check((string) $value)) {
$fail('The CAPTCHA code is incorrect or has expired.');
}
}
}
Usage:
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email'],
'captcha_code' => ['required', 'string', new ValidCaptcha],
]);
For Laravel versions before 10, implement Rule with passes() and message() instead.
Default Securimage stores the expected code in $_SESSION. That is awkward in Laravel because Laravel's session driver does not populate PHP's native $_SESSION. If you run multiple app servers, native session storage is also unreliable.
The more Laravel-friendly pattern is Securimage's CAPTCHA ID plus database storage:
use_database, configure pdo_dsn, pdo_user, pdo_pass, and the table.captchaId query parameter.captchaId in a hidden form field.Securimage::checkByCaptchaId($captchaId, $value) or the instance equivalent, depending on your version.Sketch:
// Controller: image
$captcha = new \Securimage(array_merge(config('securimage'), [
'use_database' => true,
'captchaId' => $request->string('captcha_id')->toString() ?: null,
]));
<input type="hidden" name="captcha_id" value="{{ $captchaId }}">
<img src="{{ route('captcha.image', ['captcha_id' => $captchaId]) }}">
Use the SQL schema shipped with the package (look under its database/ or examples/ directory) and create a migration for it. This gives you one-time codes, expiry checks, and load-balancer safety without depending on PHP sessions.
If you stick with session storage, you must make $_SESSION work independently of Laravel's session. Do this on both the image route and the validation path:
if (session_status() === PHP_SESSION_NONE) {
session_name('SECURIMAGE');
session_start();
}
$captcha = new \Securimage(config('securimage'));
Be careful: this creates a second session cookie (SECURIMAGE) alongside Laravel's session cookie. That is acceptable if scoped and documented, but it is fragile under Octane, serverless, and multi-node deployments. Prefer database storage for anything beyond a single-server app.
CAPTCHA images are frequently cached by browsers or proxies. Always add a unique query parameter when refreshing:
const img = document.getElementById('captcha-image');
img.src = `${img.dataset.src}?t=${Date.now()}`;
If you use captchaId, generate a new ID on each refresh so the server stores a fresh code instead of overwriting or reusing a stale one.
Wire a separate route for audio:
public function audio()
{
$captcha = new \Securimage(config('securimage'));
ob_start();
$captcha->outputAudioFile();
$body = ob_get_clean();
return response($body, 200, [
'Content-Type' => 'audio/wav',
'Cache-Control' => 'no-store',
]);
}
Provide the audio alternative next to the image. CAPTCHA-only visual challenges create accessibility and compliance problems.
Wrap the image, input, refresh button, and validation error into one reusable component:
{{-- resources/views/components/captcha.blade.php --}}
<div class="captcha">
<img src="{{ route('captcha.image') }}?t={{ now()->timestamp }}"
alt="CAPTCHA"
id="captcha-image">
<button type="button" onclick="document.getElementById('captcha-image').src='{{ route('captcha.image') }}?t=' + Date.now()">
New code
</button>
<a href="{{ route('captcha.audio') }}" target="_blank">Audio CAPTCHA</a>
<input type="text" name="captcha_code" required autocomplete="off">
@error('captcha_code') <span>{{ $message }}</span> @enderror
</div>
Do not scrape the image in tests. Add an application-level switch:
// config/securimage.php
'enabled' => env('SECURIMAGE_ENABLED', true),
Then short-circuit in the rule:
if (! config('securimage.enabled', true)) {
return;
}
Set SECURIMAGE_ENABLED=false in phpunit.xml and local .env. Alternatively, bind a fake validator implementation in the container.
CAPTCHA is not a substitute for rate limiting. Apply throttle middleware to both the image and audio routes, and consider limiting by IP and route. CAPTCHA endpoints are themselves expensive because they generate images and audio.
The last release was 2020-05-30. Treat it as stable but effectively unmaintained. Expect deprecation notices on PHP 8.1+ and test carefully. If you hit fatal errors, the practical options are:
Do not assume modern PHP compatibility or active security patches.
$_SESSION does not mean Laravel sessionThis is the biggest integration trap. Securimage writes codes to $_SESSION, but Laravel's file/database/redis session driver does not populate PHP's native $_SESSION array. You will see a working image but validation that always fails if you do not bridge sessions or use database storage.
Symptoms:
check() always returns false.$_SESSION['securimage_code_value'] is missing in the validation request.Fix by choosing one storage strategy explicitly: native session bridge, PDO storage with captchaId, or a custom storage adapter.
A successful validation normally removes the code, and codes expire after expiry_time seconds (default around 900). This is correct behavior, but it means:
Surface a clear "code expired, request a new one" message rather than a generic invalid-code error.
Check the case_sensitive option. The default is typically false, but if it is enabled anywhere in your config, users will be confused by case failures. Pick one behavior and document it.
font_file and audio_path must be real, readable absolute paths. Relative paths and Laravel asset() URLs do not work. When deploying to a container or Forge/Envoyer release directory, base_path() changes on each release, so do not hard-code paths.
Image endpoints commonly break because:
show() emits its own Content-Type.exit inside show() bypasses Laravel's response lifecycle.Use ob_start()/ob_get_clean() where possible, set explicit headers, and ensure APP_DEBUG output cannot leak into the binary response. If the library calls exit, isolate it in a route that intentionally ends the request.
Set Cache-Control: no-store, no-cache, must-revalidate on CAPTCHA responses. Exclude /captcha/* from CDN caching and from any HTTP cache middleware. A stale CAPTCHA image paired with a fresh stored code is a classic production bug.
captchaId input validationIf you accept a captchaId from the request, validate it strictly, e.g. alpha_dash and a maximum length. Even if the package uses prepared statements, rejecting malformed IDs early avoids obscure failures and reduces abuse surface.
Under Laravel Octane, static class state and native $_SESSION are risky. Prefer the database/captchaId flow, close native sessions after use, and avoid caching Securimage instances in singletons.
If two forms each need a CAPTCHA, use separate namespaces or separate captchaId values. Otherwise the second image overwrites the first code and one form will always fail.
The audio CAPTCHA requires the bundled .wav files and a readable audio_path. In containerized deployments, confirm the files were not excluded by .gitignore or Docker build rules. Also check that the generated audio response has the correct MIME type.
Image generation can fail because of missing GD, missing fonts, or permission issues. A raw PHP warning can turn a CAPTCHA response into an HTML error page that the browser tries to render as an image. Wrap generation in a try/catch or check prerequisites at boot, and log the real error rather than emitting it.
Avoid logging the generated code, the captchaId plus code together, or full validation payloads. CAPTCHA values are short-lived secrets.
Always provide an audio alternative and a visible refresh control. Do not make the CAPTCHA the only barrier between a user and account recovery. Keep code length moderate; an overly distorted 8-character code creates support burden without meaningfully improving bot resistance.
The practical customization points are:
Securimage to override code generation or rendering behavior.CaptchaService so validation, testing, and future replacement are centralized.Given the package's age, keep the integration surface small and behind an adapter. That keeps a future migration to a maintained CAPTCHA package from touching every controller and form.
How can I help you explore Laravel packages today?