Creating a Simple Honeypot Field in Laravel

April 11, 2023 —John Koster

In this blog post, we'll cover a simple method to implement a honeypot form mechanism. A honeypot is a simple security measure designed to catch bots and automated scripts using an inconspicuous form field that human users should leave empty. We will implement our honeypot in two different ways: the first method will be to create a custom middleware class to handle it for us, and the second one will perform our logic directly within the action of our form.

#Setting up our Form and Routes

To start, we will create a simple route within our routes/web.php file we can use as the action of our form:

1<?php
2 
3use Illuminate\Support\Facades\Route;
4 
5Route::get('/', function () {
6 return view('welcome');
7});
8 
9Route::post('submission', function () {
10 // We will add to this later.
11})->name('submit');

We named our route "submit" to make it easier to reference later when building our template. We will work with the resources/views/welcome.blade.php file to simplify implementing and testing our honeypot feature. For our example, we do not need anything overly complicated to build and test our honeypot's functionality; the following Blade template will be all that we need for now:

1<!DOCTYPE html>
2<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <title>Honeypot Example using Laravel</title>
7 </head>
8 <body>
9 <form method="POST" action="{{ route('submit') }}">
10 @csrf
11 
12 <div style="display:none;">
13 <input type="text" name="honeypot" id="honeypot" value="">
14 </div>
15 
16 <button type="submit">Submit</button>
17 </form>
18 </body>
19</html>

In our sample template, on line 9, we used Laravel's route helper method to generate the URL for our named route. Additionally, on line 10, we used the @csrf directive to generate the hidden CSRF token field for us; the remainder of the form template is relatively simple, except for the honeypot field.

Our honeypot field is wrapped inside a div that has an inline style setting its display to none. This is to hide the form from the user, and the theory is that they will not see it and will skip filling it in; any automated systems blindly filling in all form fields will enter a value for this field, alerting us that it is likely a spam submission. However, the question here is, why not add this style to the input itself?

We've added the style to the div content element since many automated bots are clever enough to recognize this when added directly to the input field and to help ensure our field is hidden for users navigating the site using assistive technologies, such as screen readers.

#Creating our Middleware

Our first example will be to create our middleware to check for the presence and value of the honeypot field. If the honeypot field is not empty, the middleware will block the request and return a 403 Forbidden response. Depending on your implementation goals, it may also be desirable to pretend the submission was successful instead of returning with a 403 reply.

Middleware in Laravel can be likened to the layers of an onion. When an HTTP request enters your application, it must pass through each middleware layer, like peeling away the onion layers. Each middleware layer can perform specific tasks, such as authentication or input validation, either allowing the request to proceed to the next layer or stopping it if certain conditions are not met. As the HTTP request moves through these layers, it gets closer to the core logic of your application (the center of the onion). This onion-like structure allows for efficient filtering and management of requests; we will use them to ensure that our honeypot field is not valuable.

To get started, we will create a new middleware class located at app/Http/Middleware/Honeypot.php with the following content:

1<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\Response;
8 
9class Honeypot
10{
11 /**
12 * Handle an incoming request.
13 *
14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
15 */
16 public function handle(Request $request, Closure $next): Response
17 {
18 if ($request->has('honeypot') && ! empty($request->input('honeypot'))) {
19 // Block the request if the honeypot field is not empty.
20 return response('Forbidden', 403);
21 }
22 
23 return $next($request);
24 }
25}

Our middleware implementation checks if the current request has the honeypot field, and if it does, it then checks if the field has a non-empty value. If both of these conditions pass, we return a 403 Forbidden response.

Now that we have our middleware implementation, we need to register it to apply it to our routes. We can add it to the $middlewareAliases inside our application's HTTP kernel class.

Inside the file app/Http/Kernel.php, we can make the following addition:

1<?php
2 
3namespace App\Http;
4 
5use Illuminate\Foundation\Http\Kernel as HttpKernel;
6 
7class Kernel extends HttpKernel
8{
9 // ...
10 
11 /**
12 * The application's middleware aliases.
13 *
14 * Aliases may be used to conveniently assign middleware to routes and groups.
15 *
16 * @var array<string, class-string|string>
17 */
18 protected $middlewareAliases = [
19 'auth' => \App\Http\Middleware\Authenticate::class,
20 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
21 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
22 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
23 'can' => \Illuminate\Auth\Middleware\Authorize::class,
24 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
25 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
26 'signed' => \App\Http\Middleware\ValidateSignature::class,
27 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
28 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
29 'honeypot' => \App\Http\Middleware\Honeypot::class,
30 ];
31}

Now that our middleware is registered, we can apply it to the route we created earlier:

1<?php
2 
3use Illuminate\Support\Facades\Route;
4 
5Route::get('/', function () {
6 return view('welcome');
7});
8 
9Route::post('submission', function () {
10 // We will add to this later.
11 dd('We got your submission!');
12})->name('submit')
13 ->middleware('honeypot');

We've applied our new middleware class to our route on line 13 and added a simple dd function call to let us know that our code has been executed.

If we load the form in our browser and click the Submit button, we should see the message "We got your submission!" in the browser. However, suppose we were to use our browser's developer tools to add a value to our hidden honeypot field. In that case, we will receive a 403 Forbidden response.

#Adding our Logic Directly to our Form's Action

Our next example will be to add our logic directly to the code that fires when our form is submitted. To do this, we will pretty much just be copying and pasting our logic into our route file and removing the middleware:

1<?php
2 
3use Illuminate\Support\Facades\Route;
4 
5Route::get('/', function () {
6 return view('welcome');
7});
8 
9Route::post('submission', function () {
10 if (request()->has('honeypot') && ! empty(request()->input('honeypot'))) {
11 // Block the request if the honeypot field is not empty.
12 return response('Forbidden', 403);
13 }
14 
15 dd('We got your submission!');
16})->name('submit');
17 ->middleware('honeypot');

I prefer the middleware approach, but adding the logic directly into the form's action can make sense if it is a one-off thing that is not needed elsewhere.

#Creating a Helper Blade Directive

Suppose we want to use our honeypot mechanism across multiple forms. Adding the hidden form input field to each form might become tedious. To help with this, we can create a custom Blade directive to simplify this process.

In our app/Providers/AppServiceProvider.php file, we can add the following to our service provider's boot method:

1<?php
2 
3namespace App\Providers;
4 
5use Illuminate\Support\Facades\Blade;
6use Illuminate\Support\ServiceProvider;
7 
8class AppServiceProvider extends ServiceProvider
9{
10 // ...
11 
12 public function boot(): void
13 {
14 // ...
15 
16 Blade::directive('honeypot', function () {
17 return <<<'HTML'
18 <div style="display:none;">
19 <input type="text" name="honeypot" id="honeypot" value="">
20 </div>
21 HTML;
22 });
23 
24 // ...
25 }
26}

Now that we have registered our custom directive, we can refactor our Blade template to the following:

1<!DOCTYPE html>
2<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <title>Honeypot Example using Laravel</title>
7 </head>
8 <body>
9 <form method="POST" action="{{ route('submit') }}">
10 @csrf
11 @honeypot
12 
13 <button type="submit">Submit</button>
14 </form>
15 </body>
16</html>

#Wrapping Up

Throughout this article, we created a simple honeypot field mechanism using Laravel. We implemented it using middleware, hard-coding the logic in our form's action, and creating a simple helper Blade directive to simplify its usage.

If you found this post helpful, consider sharing it with others! 😊

Some absolutely amazing
people

The following amazing people help support this site and my open source projects ♥️
If you're interesting in supporting my work and want to show up on this list, check out my GitHub Sponsors Profile.