How to reduce spam in comments with a honeypot in PHP and JS in WordPress

Contents

Why use a honeypot for WordPress comment spam?

Spam bots typically fill every input they find in an HTML form. A honeypot is a form field that humans never fill because it is hidden or made inaccessible in the page UI, but bots that parse and populate inputs will write to it. On the server side, seeing any content in that field is a reliable indicator of automated submissions. Honeypots are low-friction (they dont require captcha or extra clicks), privacy-friendly and very effective when combined with a couple of small checks (timing, JS presence, logging).

Design goals and tradeoffs

  • Non-intrusive: avoid breaking legitimate users or accessibility. Use screen-reader-friendly hiding or off-screen hiding (left:-9999px) rather than display:none if you want screen readers to read it — but that might show to some users. Choose approach depending on your audience.
  • Server-side enforcement: never trust client-side checks alone. Always verify the honeypot on the server (PHP) before accepting a comment.
  • Compatibility with caching: if your pages are cached, injecting dynamic tokens/timestamps into the form needs care.
  • Low false positives: avoid blocking users who have JS disabled unless you intentionally want to require JS.

Overview of the implementation

  1. Add a honeypot input to the comment form (via theme or plugin).
  2. Hide it from human users via CSS or off-screen placement and mark it for screen readers when appropriate.
  3. Optionally add a hidden JS token or timestamp to detect bots that dont run JS and to detect extremely fast submissions.
  4. On the server, hook into WordPress comment processing (preprocess_comment or pre_comment_on_post) and reject submissions that fail the honeypot tests.
  5. Log spam attempts for analysis and tuning.

Simple, robust example (recommended starting point)

This example shows a minimal honeypot plus optional timestamp and JS flag. It is intended to be dropped into your themes functions.php or in a small mu-plugin. It uses server-side checks and harmless user-facing messages. Customize the field names if you prefer.

Step 1 — Add the honeypot fields to the comment form

Add a visible-to-bots but hidden-to-humans text input named (for example) hp_email. Also add two hidden fields: one for a JS indicator (hp_js) and one for a timestamp (hp_ts) generated on the server. The JS will set hp_js to 1 when JS runs and could update hp_ts if you prefer a client timestamp approach.


Step 2 — Client-side JS to set the JS flag

This JavaScript sets the hp_js field to 1 when the DOM is ready. Bots that do not execute JS will not set this flag. Do not use JS-only enforcement unless you intend to require JS for all commenters (some users may have JS disabled). The JS here is progressive — it just sets a flag that you can optionally use as part of a scoring or timing check on the server.

// Place this snippet in your themes JS file or enqueue it properly.
// It sets the hidden hp_js flag so the server knows JS ran.
document.addEventListener(DOMContentLoaded, function () {
    var hp = document.getElementById(hp_js)
    if (hp) {
        hp.value = 1
    }
    // Optional: remove or hide the explicit honeypot input to further reduce accidental interaction:
    var hpField = document.getElementById(hp_email)
    if (hpField  hpField.parentNode) {
        // Keep it in the DOM for bots, but hide visually for users (already off-screen).
        // Example: convert it to type=hidden to minimize any visibility:
        try {
            hpField.type = hidden
        } catch (e) {
            // Some browsers may not allow changing type fallback to inline style
            hpField.style.display = none
        }
    }
}, false)

Step 3 — Server-side validation before comment insert

Hook into preprocess_comment to inspect the POST data before WordPress saves the comment. If the honeypot field is filled, treat submission as spam and abort. You can use wp_die() for a simple rejection (WordPress will show an error) or set the comment to spam via wp_handle_comment_submission or by returning a WP_Error. This example logs details to debug log for later analysis.

 obvious bot
    if ( ! empty( _POST[hp_email] ) ) {
        // Log details for debugging/tuning
        error_log( sprintf(
            [Honeypot] Blocked comment from IP %s, UA %s, hp_email value: %s,
            isset(_SERVER[REMOTE_ADDR]) ? _SERVER[REMOTE_ADDR] : unknown,
            isset(_SERVER[HTTP_USER_AGENT]) ? _SERVER[HTTP_USER_AGENT] : unknown,
            is_array(_POST[hp_email]) ? json_encode(_POST[hp_email]) : _POST[hp_email]
        ) )

        // Option A: reject with message
        wp_die( Your comment looks like spam and was blocked. )

        // Option B: mark as spam and return (uncomment instead of wp_die)
        // add_filter( pre_comment_approved, function() { return spam } )
        // return commentdata
    }

    // 2) Optional: timing check (too-fast submissions are suspect)
    if ( isset( _POST[hp_ts] ) ) {
        submitted_at = intval( _POST[hp_ts] )
        now = time()
        delta = now - submitted_at
        // If less than 3 seconds, likely automated (adjust threshold as needed)
        if ( delta < 3 ) {
            error_log( sprintf([Honeypot] Too-fast comment submission: %ss, IP %s, UA %s, delta, _SERVER[REMOTE_ADDR], _SERVER[HTTP_USER_AGENT]) )
            wp_die( Comment submission was too fast please try again. )
        }
    }

    // 3) Optional: JS presence check (do not require unless you want to force JS)
    if ( isset( _POST[hp_js] )  _POST[hp_js] !== 1 ) {
        // Many legitimate users have JS disabled, so be conservative:
        // Option: increase suspicious score but dont block.
        // Example: mark as pending moderation instead of automatically publishing:
        if ( isset( commentdata[comment_approved] ) ) {
            commentdata[comment_approved] = 0 // set to pending
        }
        // Alternatively, you could call wp_die() to enforce JS-only commenting.
    }

    return commentdata
}
?>

Improved variant: JS-injected honeypot (avoids cached pages problems)

If your page HTML is cached and the server-generated timestamp/honeypot gets cached too, the simple server-side timestamp becomes inaccurate. One approach to avoid cache coupling is to insert the honeypot via JavaScript. Inject the hidden field and JS flag dynamically after page load. This prevents bots that only parse static HTML from seeing the honeypot, but also prevents false positives from cached timestamps. Be aware that JS-only injection will not trap bots that do execute JS (some advanced bots do), and it will not affect users without JS.

// JS-inject example: inject a honeypot input dynamically
(function () {
    var form = document.querySelector(form.comment-form, form#commentform)
    if (!form) return

    // Create honeypot input
    var p = document.createElement(p)
    p.style.position = absolute
    p.style.left = -9999px
    p.style.top = auto
    p.style.width = 1px
    p.style.height = 1px
    p.style.overflow = hidden

    var label = document.createElement(label)
    label.setAttribute(for, hp_email_js)
    label.textContent = Leave this field empty

    var input = document.createElement(input)
    input.type = text
    input.name = hp_email
    input.id = hp_email_js
    input.tabIndex = -1
    input.autocomplete = off

    p.appendChild(label)
    p.appendChild(input)
    form.appendChild(p)

    // Add js flag
    var jsFlag = document.createElement(input)
    jsFlag.type = hidden
    jsFlag.name = hp_js
    jsFlag.value = 1
    form.appendChild(jsFlag)
})()

Plugin-style single-file implementation (drop-in)

If you prefer a plugin file you can drop into wp-content/mu-plugins or wp-content/plugins, below is a minimal plugin header and the same logic as above. Save it as e.g. honeypot-comments.php.


Testing your honeypot

  1. Open a post page with the comment form in an incognito window and submit a normal comment — it should work as before.
  2. Manually fill the honeypot field (use devtools to reveal or change its type to text if you made it hidden) and submit — the comment must be blocked or set to spam/pending depending on your logic.
  3. Disable JavaScript and submit a comment if you are using hp_js logic — confirm whether you expect non-JS users to be allowed (pending) or blocked.
  4. Try a very-fast submission (automate a POST or paste the form and submit immediately) to verify timing checks fire as expected.
  5. Check your PHP error_log for the debug entries produced by the plugin/example code to confirm logging works.

Handling caching

If your site uses page caching (Varnish, Nginx microcaching, plugin caching) then server-injected dynamic data like time-based tokens will be the same for many visitors. Consider one of these approaches:

  • Inject the honeypot and JS flag via JavaScript (client-side injection) so the cached HTML is generic. This reduces false positives from cached timestamps but relies on JS.
  • Use server-side injection but mark pages with comments as uncached. This can be heavy for high-traffic sites.
  • Use a server timestamp but set tolerant thresholds (so an aged timestamp doesnt falsely flag a submission) and prefer not to outright block based on it.

Accessibility and naming considerations

  • Field naming: bots often recognize inputs named honeypot, hp, antispam etc. Use plausible field names (for example phone or website_confirm) that look like real fields but will not be filled by normal commenters. Avoid names that break data privacy expectations.
  • Screen readers: if you hide the field with left:-9999px it will still be available to screen readers. If you use display:none, screen readers wont see it. Choose depending on whether you want the field accessible to assistive technologies.
  • Labels: include a clear label in the HTML for form semantics. Use tabindex=-1 on the honeypot input to avoid keyboard focus.

Advanced techniques and improvements

  • Score-based system: Instead of a binary block you can increment a spam score when honeypot is filled, JS flag missing, or submission was too fast. Combine with Akismet or other spam checks and only block when score exceeds a threshold.
  • Rate limiting: log repeated honeypot hits from an IP and temporarily block the IP via firewall or fail2ban integration (parse logs and ban persistent offenders).
  • Invisible CSS traps: include several hidden inputs with different interesting names bots that fill many fields are more likely to be malicious.
  • Use cookies or localStorage: set a cookie via JS and require it on submission. Bots that dont handle cookies will be caught — but this can penalize users who disallow cookies.
  • REST / AJAX comments: if comments are submitted via AJAX or the WordPress REST API, ensure your check also covers those endpoints by hooking into the appropriate filters or validating the payload on the server-side.

Logging and monitoring

Collect and review logs of blocked attempts for a while after enabling a honeypot — it helps tune thresholds and field names. Use error_log for small sites or push logs into a central logging system for larger deployments. When you see false positives, adjust your logic (e.g., dont outright block for missing JS instead set to pending).

Common pitfalls and how to avoid them

  • False positives: Blocking legit users who use assistive tech or have JS disabled. Avoid aggressive blocking based solely on missing JS or a single heuristic.
  • Caching issues: stale timestamps or tokens due to page cache. Use JS injection or avoid using server timestamps as the only check.
  • Theme/plugin compatibility: Some comment plugins or themes use custom comment templates make sure hooks you use are firing. If not, add the honeypot directly to the comment template or adapt to the plugin’s hooks.
  • Bots evolving: Honeypots are effective but not foolproof — combine with other anti-spam measures (Akismet, rate-limiting, ReCaptcha as last resort) for defense in depth.

Summary and recommended setup

  1. Add a simple honeypot input server-side and hide it via off-screen CSS or turn it into type=hidden via JS.
  2. Set a JS flag on DOM ready and optionally use a timestamp to detect too-fast submissions.
  3. Always validate on the server with preprocess_comment log blocked attempts for analysis.
  4. Be conservative with user-facing blocks: prefer setting suspicious comments to pending moderation rather than immediate deletion unless you are confident in the heuristic.
  5. Monitor and adjust field names and thresholds periodically to maintain effectiveness.

Example quick checklist before deploying

  • Back up functions.php or install as a small plugin.
  • Test normal commenting flow as a logged-out user and as a logged-in user.
  • Test with JS disabled to see behavior (and decide policy).
  • Verify caching behavior — ensure timestamps or injected fields work as intended.
  • Check debug logs for honeypot hits after launch.

References and further reading

Use official WordPress documentation for hooks referenced here (preprocess_comment, comment_form hooks) to adapt code to your environment. Consider combining honeypot logic with existing anti-spam services for layered protection.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

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