PHP Contact Form With Spam Protection: Build It in 4 Layers

Santaji GadePHPDevelopmentYesterday5 Views

PHP Contact Form

Build a php contact form with spam protection combining a honeypot field, IP rate limiting, server side validation, a CSRF token, and reCAPTCHA v3.

Development PHP Contact Forms Spam Protection

A contact form with no defenses fills an inbox with junk within days of going live, long before a real customer ever finds it. A php contact form with spam protection stays useful because the bot traffic never reaches the inbox at all.

01

Why a PHP Contact Form With Spam Protection Beats a Bare HTML Form

A plain HTML form with a PHP handler behind it works the moment it ships. It also has no idea whether the request came from a person or a script.

Automated bots crawl the web looking for exactly this pattern, an open form action pointing at a PHP file with no checks in front of it. Cloudflare's overview of bot traffic gives a good sense of just how much of the web this kind of scanning actually covers.

Within a short time, a completely unprotected form starts receiving dozens of submissions a day that are not from real visitors at all.

Each layer added below blocks a different kind of automated traffic, and together they cover almost everything a small site actually sees.

02

Setting Up the HTML Form and PHP Handler

The form itself stays simple. Every protection layer in this guide lives in the PHP file that processes the submission, not in extra form fields the visitor has to fill out.

contact.html
<form action="submit.php" method="POST">
  <input type="text" name="name" required>
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <input type="text" name="website" class="bx-hidden-field" autocomplete="off" tabindex="-1">
  <button type="submit">Send</button>
</form>

That extra website field is not a mistake. It is the honeypot the next section relies on, and it needs to already exist in the markup.

03

Adding a Honeypot Field to Catch Bots

A honeypot is a form field real visitors never see or fill in, but that most automated bots fill in anyway since they do not render the page.

submit.php
if (!empty($_POST['website'])) {
    // a filled honeypot means a bot, so exit quietly with no error
    http_response_code(200);
    exit;
}

Hiding the field with CSS instead of type="hidden" matters, since some bots specifically skip inputs with a hidden type but still fill in a text field they can see in the raw markup. Setting tabindex to a negative value also keeps the field out of the normal keyboard navigation order for real visitors.

style.css
.bx-hidden-field {
  position: absolute;
  left: -9999px;
}
04

Rate Limiting Submissions by IP

A honeypot stops simple bots, but a more determined script can still hammer the form with dozens of submissions a minute from the same address.

rate_limit.php
function is_rate_limited($ip, $max_per_hour = 5) {
    $file = sys_get_temp_dir() . '/rate_' . md5($ip) . '.json';
    $now = time();
    $hits = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
    $hits = array_filter($hits, fn($t) => $now - $t < 3600);

    if (count($hits) >= $max_per_hour) {
        return true;
    }

    $hits[] = $now;
    file_put_contents($file, json_encode($hits));
    return false;
}

A five submission per hour limit is generous for a real visitor filling out a contact form once, but tight enough to stop a script from flooding the inbox. PHPMailer itself, available through Packagist and installed with Composer, still handles the actual sending once a message clears every check in this guide.

05

Validating and Sanitizing Input Server Side

Client side validation is easy to bypass, since a bot skips the browser entirely and sends a POST request directly to the PHP file.

validate.php
$name = trim(filter_input(INPUT_POST, 'name', FILTER_SANITIZE_SPECIAL_CHARS));
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$message = trim(filter_input(INPUT_POST, 'message', FILTER_SANITIZE_SPECIAL_CHARS));

if (!$name || !$email || !$message) {
    exit('Please fill in every field with a valid value.');
}

FILTER_VALIDATE_EMAIL catches most malformed addresses on its own, and rejecting the submission outright when it fails keeps obviously fake entries out of the message.

LayerStopsVisitor Friction
Honeypot fieldSimple form scraping botsNone, invisible to real visitors
Rate limiting by IPScripted flooding attacksNone for normal one time submissions
Server side validationMalformed or fake dataNone, just standard field rules
reCAPTCHA v3Sophisticated bots and scrapersVery low, runs silently in the background
06

Protecting the Form With a CSRF Token

A cross site request forgery token confirms the submission actually came from the form on your own site, not from a request forged on another page. OWASP's CSRF prevention guide covers the reasoning behind this in more depth than a single contact form needs, but it is worth reading once.

csrf.php
session_start();
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// echo this into a hidden input named csrf_token when rendering the form

if ($_POST['csrf_token' ?? ''] !== $_SESSION['csrf_token']) {
    http_response_code(403);
    exit('Invalid request.');
}

random_bytes() generates a cryptographically strong token, which is a meaningfully different guarantee than a predictable value like a timestamp would give.

07

Adding Google reCAPTCHA v3 for a Final Layer

The layers above stop most automated traffic. reCAPTCHA v3 catches the more sophisticated bots that get past everything else, and it runs invisibly in the background. Registering a site key takes a few minutes through the reCAPTCHA admin console.

verify_recaptcha.php
function verify_recaptcha($token, $secret) {
    $response = file_get_contents(
        'https://www.google.com/recaptcha/api/siteverify?secret=' . $secret . '&response=' . $token
    );
    $result = json_decode($response, true);
    return ($result['success'] ?? false) && ($result['score'] ?? 0) >= 0.5;
}

The score returned ranges from 0 to 1, and a threshold around 0.5 is a reasonable starting point that most sites can tune later based on real traffic.

08

Sending the Verified Message With PHPMailer

Once a submission has passed every check above, sending it uses the same PHPMailer pattern this series has covered for writing send email code in PHP, an SMTP call wrapped in a try and catch block.

send_message.php
if (!is_rate_limited($_SERVER['REMOTE_ADDR']) && verify_recaptcha($_POST['g_recaptcha_response'], $secret)) {
    send_notification_email('contact@brandella.in', 'New contact form message', $message);
}

Every layer runs before this final step, so by the time send_notification_email() executes, the message has already survived four separate checks. This is the complete shape of a php contact form with spam protection, four layers working together instead of one control doing all the work.

  • Never trust the honeypot alone: a determined bot can eventually learn to skip a hidden field, so layer it with rate limiting and validation.
  • Store the reCAPTCHA secret as an environment variable: it belongs alongside SMTP credentials, never committed to the repository.
  • Log rejected submissions briefly: a short log of blocked attempts helps confirm the protections are actually catching something.
  • Keep error messages generic: telling a bot exactly which check failed only helps it adjust and try again.
  • Review the reCAPTCHA score threshold periodically: real visitor behavior varies by site, so 0.5 is a starting point, not a fixed rule.
09

Watch: Building a PHP Contact Form

Traversy Media's PHP series walks through a working contact form handler, which pairs well with the honeypot and validation layers added in this guide.

Video credit: Traversy Media.

The base handler shown in the video is a solid starting point once the honeypot, rate limiting, and reCAPTCHA layers from this guide sit in front of it.

Form Submit Honeypot Rate Limit reCAPTCHA v3 Inbox Delivery

Frequently Asked Questions

It stops simple bots but not every one. Pairing it with rate limiting and reCAPTCHA covers the more determined scripts a honeypot alone will miss.

No. The rate limiting example in this guide uses a temporary file per IP address, which is enough for a small to medium site without any database setup.

No. Version 3 runs entirely in the background and returns a score, unlike the older checkbox based version 2 that visitors had to click.

A five submission per hour limit rarely affects a genuine visitor, since nobody normally submits the same contact form that many times in one session.

Yes. Without it, another site could submit forged requests to your form handler using a visitor's own browser session without their knowledge.

What We Learn Today

1

Layers beat a single defense

No single check stops every kind of automated traffic on its own.

2

A honeypot is invisible by design

Real visitors never notice it, but simple bots fill it in anyway.

3

Rate limiting needs no database

A temporary file per IP address is enough for most small sites.

4

Server side validation is not optional

Client side checks alone are trivial for a bot to bypass entirely.

5

CSRF tokens confirm the source

They prove a submission came from your own form, not a forged request.

6

reCAPTCHA v3 adds a final filter

It catches the sophisticated bots everything else lets through.

Ready to Stop Spam on Your Contact Form?

Pair a php contact form with spam protection alongside a wider toolkit to keep every inbound message legitimate.

0 Votes: 0 Upvotes, 0 Downvotes (0 Points)

Leave a reply

Loading Next Post...
Search
Popular Now
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...