Honeypot and time-trap middleware
Block simple form bots with a hidden field and an encrypted render timestamp, rejecting submissions that are filled in too fast.
LaravelMiddlewarespamhoneypotmiddlewareforms
Code
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
class SpamTrap
{
/** Humans need a few seconds to fill a form; bots post instantly. */
private const MIN_SECONDS = 3;
private const MAX_SECONDS = 7200;
public function handle(Request $request, Closure $next): Response
{
// 1. Honeypot: a field hidden with CSS that real visitors never fill.
if ($request->filled('website_url')) {
return $this->fake($request);
}
// 2. Time trap: the token is the encrypted moment the form was rendered.
try {
$age = time() - (int) Crypt::decryptString((string) $request->input('_rendered'));
} catch (Throwable) {
return $this->fake($request);
}
if ($age < self::MIN_SECONDS || $age > self::MAX_SECONDS) {
return $this->fake($request);
}
return $next($request);
}
/** Look successful so the bot does not learn which check it failed. */
private function fake(Request $request): Response
{
return $request->expectsJson()
? response()->json(['ok' => true])
: back()->with('status', 'Thanks, we received your message.');
}
}
{{-- Put inside every public <form>. Usage: <x-spam-trap /> --}}
<div aria-hidden="true" style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
<label>Leave this empty
<input type="text" name="website_url" value="" tabindex="-1" autocomplete="off">
</label>
</div>
<input type="hidden" name="_rendered" value="{{ \Illuminate\Support\Facades\Crypt::encryptString((string) time()) }}">
How it works
What it solves
Contact forms and public surveys attract bots that fill every input and post immediately. Visible CAPTCHAs hurt conversion, so this uses two invisible checks that stop the large majority of unsophisticated bots without bothering real people.
How it works
The Blade component renders a text input named website_url inside a container moved off-screen, with tabindex="-1" and aria-hidden, so keyboard and screen reader users never meet it. Bots that fill every field give themselves away. It also renders a hidden _rendered field holding the render time, encrypted with the app key.
The middleware first rejects any request where the honeypot has a value. Then it decrypts the timestamp and computes the age. A tampered or missing token fails to decrypt and is rejected; an age under three seconds means the form was posted faster than a person could type; an age over two hours means a stale page. Because the timestamp is encrypted, a bot cannot forge an older one.
Rejected requests receive a fake success response, so the sender gets no signal to adapt to.
Adapting it
- Rename
website_urlper form, or generate it from a seed, to defeat bots that learn the name. - Log rejected attempts with
Log::infoto measure how much spam you stop. - Apply with
Route::post('/contact', ...)->middleware(SpamTrap::class).
Pitfalls
- Password managers may autofill a field called
website; keep an unusual name. - Cached form pages freeze the timestamp, so never cache pages that include this component.
How to use it
- Save the Blade file as
resources/views/components/spam-trap.blade.php. - Put
SpamTrap.phpinapp/Http/Middleware/. - Add
<x-spam-trap />inside each public form, next to@csrf. - Attach the middleware to the POST route:
->middleware(\App\Http\Middleware\SpamTrap::class). - Do not full-page cache the form route.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.