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:
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.