Laravel Contact Form Spam Protection: A Complete Guide

How to Add Effective Spam Protection to Your Contact Form (No API Keys Required) Laravel Contact Form Spam Protection

As a Laravel developer, you’ve probably experienced the frustration of spam submissions flooding your contact forms. One day, you’re excited about your new website, and the next day, your inbox is drowning in fake messages about cryptocurrency investments and dubious pharmaceutical offers. You are one of the many (all) of us out there!

The good news is that Laravel provides excellent tools to combat spam without requiring external APIs or expensive third-party services. In this guide, we’ll build a comprehensive spam protection system using Laravel’s built-in features, Blade templates, and some clever techniques that work surprisingly well.

Why Laravel Makes Spam Protection Easy?

Laravel’s ecosystem gives us everything we need:

  • CSRF protection built in (that @csrf directive you always use)
  • Session management for storing temporary data
  • Validation system for server-side checks
  • Middleware for rate limiting
  • Blade directives for clean template code

Let’s build a contact form that stops spam dead in its tracks.

What the Final Result Looks Like

Before we dive into the code, here’s what your spam-protected contact form will look like to users. Notice how the protection features are integrated seamlessly into a clean, professional interface:

laravel_form_spam_protection

 

The form includes visible security elements like the math CAPTCHA and human verification checkbox, while the more sophisticated protection (honeypot, timing, rate limiting) works invisibly in the background.

Setting Up Your Basic Contact Form

First, let’s start with a typical Laravel contact form. If you’re following along, create a new route in routes/web.php:

@extends(‘layouts.app’)

@section(‘content’)
<div class=”container”>
<div class=”row justify-content-center”>
<div class=”col-md-8″>
<div class=”card”>
<div class=”card-header”>Contact Us</div>
<div class=”card-body”>
@if(session(‘success’))
<div class=”alert alert-success”>{{ session(‘success’) }}</div>
@endif

@if(session(‘error’))
<div class=”alert alert-danger”>{{ session(‘error’) }}</div>
@endif

<form method=”POST” action=”{{ route(‘contact.store’) }}” id=”contact-form”>
@csrf

{{– Honeypot field – invisible to humans, irresistible to bots –}}
<div style=”display: none;”>
<input type=”text” name=”website” tabindex=”-1″ autocomplete=”off”>
</div>

{{– Time tracking for submission speed detection –}}
<input type=”hidden” name=”form_start_time” value=”{{ time() }}”>

<div class=”row”>
<div class=”col-md-6″>
<div class=”form-group”>
<label for=”fname”>First Name</label>
<input type=”text” class=”form-control @error(‘fname’) is-invalid @enderror”
id=”fname” name=”fname” value=”{{ old(‘fname’) }}”
maxlength=”50″ required>
@error(‘fname’)
<span class=”invalid-feedback”>{{ $message }}</span>
@enderror
</div>
</div>

<div class=”col-md-6″>
<div class=”form-group”>
<label for=”lname”>Last Name</label>
<input type=”text” class=”form-control @error(‘lname’) is-invalid @enderror”
id=”lname” name=”lname” value=”{{ old(‘lname’) }}”
maxlength=”50″ required>
@error(‘lname’)
<span class=”invalid-feedback”>{{ $message }}</span>
@enderror
</div>
</div>
</div>

<div class=”form-group”>
<label for=”email”>Email Address</label>
<input type=”email” class=”form-control @error(’email’) is-invalid @enderror”
id=”email” name=”email” value=”{{ old(’email’) }}”
maxlength=”100″ required>
@error(’email’)
<span class=”invalid-feedback”>{{ $message }}</span>
@enderror
</div>

<div class=”form-group”>
<label for=”message”>Your Message</label>
<textarea class=”form-control @error(‘message’) is-invalid @enderror”
id=”message” name=”message” rows=”5″
maxlength=”1000″ required>{{ old(‘message’) }}</textarea>
@error(‘message’)
<span class=”invalid-feedback”>{{ $message }}</span>
@enderror
</div>

{{– Simple math CAPTCHA –}}
<div class=”form-group”>
<label for=”captcha”>Security Check: What is {{ $num1 }} + {{ $num2 }}?</label>
<input type=”number” class=”form-control @error(‘captcha_answer’) is-invalid @enderror”
id=”captcha” name=”captcha_answer” required>
@error(‘captcha_answer’)
<span class=”invalid-feedback”>{{ $message }}</span>
@enderror
</div>

{{– Human verification checkbox –}}
<div class=”form-group”>
<div class=”form-check”>
<input class=”form-check-input @error(‘human_check’) is-invalid @enderror”
type=”checkbox” id=”human_check” name=”human_check” required>
<label class=”form-check-label” for=”human_check”>
I confirm I am not a robot
</label>
@error(‘human_check’)
<span class=”invalid-feedback”>{{ $message }}</span>
@enderror
</div>
</div>

<button type=”submit” class=”btn btn-primary” id=”submit-btn”>
Send Message
</button>
</form>
</div>
</div>
</div>
</div>
</div>

<script>
// Prevent rapid-fire submissions
document.getElementById(‘contact-form’).addEventListener(‘submit’, function(e) {
const submitBtn = document.getElementById(‘submit-btn’);
submitBtn.disabled = true;
submitBtn.textContent = ‘Sending…’;

// Re-enable after 5 seconds in case of validation errors
setTimeout(() => {
submitBtn.disabled = false;
submitBtn.textContent = ‘Send Message’;
}, 5000);
});
</script>
@endsection

The Controller: Let’s write some backend code now

Now let’s create the ContactController with our spam protection logic:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Log;

class ContactController extends Controller
{
public function index()
{
// Generate simple math CAPTCHA
$num1 = rand(1, 10);
$num2 = rand(1, 10);

// Store the correct answer in session
Session::put(‘captcha_answer’, $num1 + $num2);

return view(‘contact’, compact(‘num1’, ‘num2’));
}

public function store(Request $request)
{
// Layer 1: Honeypot Protection
if (!empty($request->website)) {
Log::warning(‘Honeypot triggered’, [‘ip’ => $request->ip()]);
return back()->with(‘error’, ‘Spam detected. Please try again.’);
}

// Layer 2: Time-based Protection (minimum 3 seconds)
$formStartTime = $request->form_start_time;
$timeTaken = time() – $formStartTime;

if ($timeTaken < 3) {
Log::warning(‘Form submitted too quickly’, [
‘ip’ => $request->ip(),
‘time_taken’ => $timeTaken
]);
return back()->with(‘error’, ‘Please take your time to fill the form.’);
}

// Layer 3: Rate Limiting (3 submissions per hour per IP)
$ipKey = ‘contact_submissions_’ . md5($request->ip());
$submissions = Session::get($ipKey, []);

// Remove submissions older than 1 hour
$submissions = array_filter($submissions, function($timestamp) {
return $timestamp > (time() – 3600);
});

if (count($submissions) >= 3) {
Log::warning(‘Rate limit exceeded’, [‘ip’ => $request->ip()]);
return back()->with(‘error’, ‘Too many submissions. Please try again later.’);
}

// Layer 4: CAPTCHA Validation
$userAnswer = (int) $request->captcha_answer;
$correctAnswer = Session::get(‘captcha_answer’);

if ($userAnswer !== $correctAnswer) {
return back()->with(‘error’, ‘Incorrect security answer.’)
->withInput();
}

// Layer 5: Content Analysis
if ($this->isSpamContent($request)) {
Log::warning(‘Spam content detected’, [
‘ip’ => $request->ip(),
‘message’ => $request->message
]);
return back()->with(‘error’, ‘Your message contains inappropriate content.’);
}

// Standard Laravel validation
$validated = $request->validate([
‘fname’ => ‘required|string|max:50’,
‘lname’ => ‘required|string|max:50’,
’email’ => ‘required|email|max:100’,
‘message’ => ‘required|string|max:1000’,
‘human_check’ => ‘required|accepted’
]);

// Record successful submission for rate limiting
$submissions[] = time();
Session::put($ipKey, $submissions);

// Clear CAPTCHA answer
Session::forget(‘captcha_answer’);

// Send email (add your email logic here)
try {
// Mail::to(‘admin@yoursite.com’)->send(new ContactMail($validated));

return back()->with(‘success’, ‘Thank you! Your message has been sent successfully.’);
} catch (\Exception $e) {
Log::error(‘Email sending failed’, [‘error’ => $e->getMessage()]);
return back()->with(‘error’, ‘Sorry, there was an error sending your message.’);
}
}

private function isSpamContent(Request $request)
{
// Common spam keywords
$spamKeywords = [
‘viagra’, ‘cialis’, ‘casino’, ‘poker’, ‘bitcoin’, ‘cryptocurrency’,
‘investment opportunity’, ‘make money fast’, ‘click here’, ‘buy now’
];

$content = strtolower($request->fname . ‘ ‘ . $request->lname . ‘ ‘ . $request->message);

// Check for spam keywords
foreach ($spamKeywords as $keyword) {
if (strpos($content, $keyword) !== false) {
return true;
}
}

// Check for excessive links (more than 2 URLs)
$linkCount = preg_match_all(‘/(http|https|www\.|\.com|\.org|\.net)/i’, $request->message);
if ($linkCount > 2) {
return true;
}

// Check for repeated characters (spam pattern)
if (preg_match(‘/(.)\1{5,}/’, $request->message)) {
return true;
}

// Check for all caps (more than 70% uppercase)
$uppercaseRatio = strlen(preg_replace(‘/[^A-Z]/’, ”, $request->message)) / strlen($request->message);
if ($uppercaseRatio > 0.7) {
return true;
}

return false;
}
}

Understanding Each Protection Layer

Let’s see how each approach works and gives us a multi-layered spam-protection system for our Laravel contact form:

1. CSRF Protection (Built-in Laravel)

That @csrf directive is your first line of defense. Laravel automatically validates this token, preventing cross-site request forgery attacks.

2. Honeypot Field

The hidden website field is invisible to humans but often filled by bots. It’s an effective way to identify automated submissions.

3. Time-based Protection

Real users need time to read and fill out forms. Bots submit forms immediately. By checking submission time, we can identify automated submissions.

4. Rate Limiting with Sessions

Laravel’s session system lets us track submission attempts per IP address. Three submissions per hour is reasonable for humans but prevents automated spam campaigns.

5. Math CAPTCHA

Instead of annoying image CAPTCHAs, we use simple math. The answer is stored in the Laravel session and validated server-side.

6. Content Analysis

We analyze the message content for common spam patterns using Laravel’s string helpers and PHP’s regex functions.

Adding Email Notifications

Now that we know how each approach helps in stopping spam via our Laravel contact form, let’s see it in action by firing an email from our Laravel contact form page when a valid form is submitted. Actually, what we are going to see here is if the form detects the entries it is supposed to stop or not!

Let’s create a Mail class to handle contact form emails:

php artisan make:mail ContactMail

Then in app/Mail/ContactMail.php let’s send the form data:

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class ContactMail extends Mailable
{
use Queueable, SerializesModels;

public $contactData;

public function __construct($contactData)
{
$this->contactData = $contactData;
}

public function build()
{
return $this->subject(‘New Contact Form Submission’)
->view(’emails.contact’)
->with(‘data’, $this->contactData);
}
}

Testing our Laravel Spam Protection

Here’s how to test each layer:

  • Honeypot: Use browser dev tools to unhide the field and fill it
  • Time protection: Submit the form immediately after loading
  • Rate limiting: Submit multiple times rapidly
  • CAPTCHA: Enter wrong math answers
  • Content analysis: Try messages with spam keywords

Advanced Tips if you want to do more

Using Middleware for Rate Limiting

Create custom middleware for more sophisticated rate limiting:

php artisan make:middleware ContactRateLimit

Logging Spam Attempts

We can go a step ahead and use Laravel’s logging system to track spam patterns:

Log::channel('spam')->warning('Spam detected', [
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'type' => 'honeypot'
]);

Database Tracking

At the database level, we can consider storing spam attempts in a database table for analysis:

php artisan make:migration create_spam_attempts_table

Configuration and Environment Variables

Add these to your .env file for easy configuration:

CONTACT_RATE_LIMIT=3
CONTACT_TIME_LIMIT=3
CONTACT_MAX_LINKS=2
CONTACT_ADMIN_EMAIL=admin@yoursite.com

What The Users Will See

The beauty of this system is that legitimate users barely notice it. They see:

  • A normal contact form
  • A simple math question
  • A single checkbox
  • Helpful error messages if something goes wrong

Meanwhile, bots encounter multiple barriers that prevent them from submitting successfully.

Common Pitfalls to Avoid

While there’s no limit to how far we can go, trying to making is more secure. But keeping tthe following things in mind will save us from overengineering the simple (even though irritating) problem. Watch out for the following:

  • Don’t be too aggressive – False positives hurt real customers
  • Always validate server-side – Never trust client-side validation alone
  • Test thoroughly – Make sure real users can still submit successfully
  • Monitor your logs – Watch for new spam patterns
  • Keep it simple – Complex systems are harder to maintain

Scaling Considerations

While we obvously should strive not to over-engineer stuff, it is also equally true that there will be times where the app grows to something myuch largerIn that case, the following can be considered:

  • Moving rate limiting to Redis or a database
  • Using Laravel’s queue system for email sending
  • Implementing more sophisticated content analysis
  • Adding IP reputation checking

The Bottom Line

Laravel provides excellent tools for spam protection without requiring external services. By combining Laravel’s built-in features with these techniques, we can eliminate most spam while maintaining a great user experience.
The key is layering multiple lightweight checks rather than relying on one heavy-handed solution. Start with the honeypot and time-based protection for immediate relief, then add other layers as needed.
Remember: The goal is never to create perfect security, but to make your contact form more effort than it’s worth for spammers while keeping it accessible for legitimate users.
Our Laravel contact form can be both user-friendly and spam-proof.

Share This Post

Leave a Comment

Your email address will not be published. Required fields are marked *

Subscribe To my Future Posts

Get notified whenever I post something new

More To Explore