WordPress Comment Security: Complete Guide (2025) - Affinite.io

Introduction: Why Comment Security Matters

WordPress comments are one of the most frequently attacked vectors on websites. According to Wordfence statistics, attacks targeting comment systems account for more than 35% of all automated attacks against WordPress. Spam comments not only degrade the user experience, they can also negatively impact SEO rankings and, in extreme cases, lead to the domain being blacklisted.

How Comment Attacks Work

Technical Background of Attacks

Modern spam bots use sophisticated methods to bypass standard protections:

1. Direct HTTP requests

  • Bots do not use the on-page form but send direct POST requests
  • This bypasses JavaScript validations, CSRF tokens, and other client-side defenses
  • They can simulate legitimate user agents and HTTP headers

2. Targeted endpoints:

  • wp-comments-post.php – the main endpoint for processing comments
  • /wp-json/wp/v2/comments – REST API endpoint (WordPress 4.7+)
  • xmlrpc.php – XML-RPC methods for pingbacks and trackbacks
  • /wp-json/wp/v2/posts/{id}/comments – specific REST endpoints for individual posts

3. Advanced techniques:

  • Rate limiting evasion — rotating IP addresses through proxy networks
  • Fingerprint spoofing — imitating legitimate browsers
  • Content spinning — automatically generating text variations
  • Honeypot detection — detecting and bypassing bot traps

Impact of Successful Attacks

Impact TypeDescriptionSeverity
PerformanceDatabase overload, site slowdownHigh
SEOSpam links, SERP degradationHigh
SecurityPotential XSS and injection attacksCritical
CostsHigher server load, cleanup effortMedium
ReputationLoss of visitor trustMedium

Comprehensive Security — Step by Step

1. Core settings in the admin

Discussion Settings (wp-admin/options-discussion.php)

Critical settings:

Settings → Discussion 

Recommended configuration:

  1. Default post settings:
    • Allow visitors to post comments on new articles
    • Only registered and logged-in users can comment
  2. Other comment settings:
    • Comment author must fill out name and email
    • Users must be registered and logged in to comment
    • Automatically close comments on posts older than 30 days
  3. Notifications & moderation:
    • Comment must be manually approved
    • Hold a comment in the queue if it contains 2 or more links
    • Email me whenever anyone posts a comment
    • Email me whenever a comment is held for moderation
  4. Blacklist settings:
    • Add common spam terms to the disallowed comment keys
    • Automatically flag comments containing suspicious keywords

Advanced WordPress hooks

Pre-comment validation:

// functions.php or MU plugin
add_action('pre_comment_on_post', 'custom_comment_security_check');
function custom_comment_security_check($comment_post_id) {
    // Check whether the user is logged in if
    if (!is_user_logged_in()) {
        wp_die(
            'Comments are allowed for logged-in users only.', 
            'Access denied', 
            ['response' => 403]
        );
    }
    
    // Check comment submission rate
    $user_id = get_current_user_id();
    $last_comment = get_transient('last_comment_time_' . $user_id);
    
    if ($last_comment && (time() - $last_comment) < 30) {
        wp_die(
            'Commenting too quickly. Please try again in a moment.', 
            'Rate limit exceeded', 
            ['response' => 429]
        );
    }
    
    set_transient('last_comment_time_' . $user_id, time(), 3600);
}

2. Technical hardening of endpoints

REST API protections

Fully block anonymous comments:

// Disable anonymous comments via REST API
add_filter('rest_allow_anonymous_comments', '__return_false');

// Add additional authentication
add_filter('rest_pre_insert_comment', 'secure_rest_comments', 10, 2);
function secure_rest_comments($prepared_comment, $request) {
    if (!is_user_logged_in()) {
        return new WP_Error(
            'rest_comment_login_required',
            'You must be logged in to comment.',
            ['status' => 401]
        );
    }
    return $prepared_comment;
}

// Optional — remove the entire comments endpoint
add_filter('rest_endpoints', 'remove_comments_endpoints');
function remove_comments_endpoints($endpoints) {
    if (isset($endpoints['/wp/v2/comments'])) {
        unset($endpoints['/wp/v2/comments']);
    }
    if (isset($endpoints['/wp/v2/comments/(?P<id>[\d]+)'])) {
        unset($endpoints['/wp/v2/comments/(?P<id>[\d]+)']);
    }
    return $endpoints;
}

Selective REST API protection:


// More advanced protection with logging
add_filter('rest_pre_dispatch', 'monitor_comments_api', 10, 3);
function monitor_comments_api($result, $server, $request) {
    $route = $request->get_route();
    
    if (strpos($route, '/wp/v2/comments') !== false) {
        // Log access attempts
        error_log(sprintf(
            'Comments API access attempt - IP: %s, User-Agent: %s, Route: %s',
            $_SERVER['REMOTE_ADDR'] ?? 'unknown',
            $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
            $route
        ));
        
        // Block for non-logged-in users
        if (!is_user_logged_in()) {
            return new WP_Error(
                'rest_comments_forbidden', 
                'Comments API access forbidden',
                ['status' => 403]
            );
        }
    }
    
    return $result;
}

XML-RPC hardening

XML-RPC is a frequent attack target, especially the pingback methods:

// Or selectively block dangerous methods
add_filter('xmlrpc_methods', 'secure_xmlrpc_methods');
function secure_xmlrpc_methods($methods) {
    // Remove dangerous methods
    unset($methods['pingback.ping']);
    unset($methods['pingback.extensions.getPingbacks']);
    unset($methods['wp.newComment']);
    
    return $methods;
}

// Additional XML-RPC protection via .htaccess
// Add to .htaccess:
/*
<Files "xmlrpc.php">
    Order Allow,Deny
    Deny from all
</Files>
*/

wp-comments-post.php hardening

PHP method (recommended):

add_action('init', 'secure_comments_post_endpoint');
function secure_comments_post_endpoint() {
    if (strpos($_SERVER['REQUEST_URI'], 'wp-comments-post.php') !== false) {
        // Referer check
        if (!wp_verify_nonce($_POST['_wp_http_referer'] ?? '', 'comment_nonce')) {
            // Log suspicious activity
            error_log(sprintf(
                'Suspicious comment attempt - IP: %s, User-Agent: %s, Referer: %s',
                $_SERVER['REMOTE_ADDR'] ?? 'unknown',
                $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
                $_SERVER['HTTP_REFERER'] ?? 'none'
            ));
        }
        
        if (!is_user_logged_in()) {
            status_header(403);
            wp_die('Comments are allowed for logged-in users only.');
        }
        
        // Rate limiting
        $ip = $_SERVER['REMOTE_ADDR'] ?? '';
        $attempts = get_transient('comment_attempts_' . md5($ip));
        
        if ($attempts && $attempts > 5) {
            status_header(429);
            wp_die('Too many attempts. Please try again later.');
        }
        
        set_transient('comment_attempts_' . md5($ip), ($attempts + 1), 300);
    }
}

.htaccess method (Apache):

<IfModule mod_rewrite.c>
    RewriteEngine On
    
    # Block wp-comments-post.php for non-logged-in users
    RewriteCond %{REQUEST_URI} ^/wp-comments-post\.php$ [NC]
    RewriteCond %{HTTP_COOKIE} !wordpress_logged_in_ [NC]
    RewriteCond %{REQUEST_METHOD} POST [NC]
    RewriteRule .* - [R=403,L]
    
    # Block suspicious user agents
    RewriteCond %{HTTP_USER_AGENT} ^$ [NC,OR]
    RewriteCond %{HTTP_USER_AGENT} (bot|crawler|spider) [NC]
    RewriteCond %{REQUEST_URI} wp-comments-post\.php$ [NC]
    RewriteRule .* - [R=403,L]
</IfModule>

Nginx variant:

location = /wp-comments-post.php {
    # Check login via cookie
    if ($http_cookie !~* "wordpress_logged_in_") {
        return 403;
    }
    
    # Rate limiting
    limit_req zone=comments burst=2 nodelay;
    
    # Block empty user agents
    if ($http_user_agent = "") {
        return 403;
    }
    
    try_files $uri =404;
    fastcgi_pass php;
}

# Rate limiting definice (add to the http block)
limit_req_zone $binary_remote_addr zone=comments:10m rate=1r/m;

3. Advanced anti-spam protection

Anti-spam plugins — comparison

PluginPriceDetection methodsProsCons
Akismet$5–50/moAI, blacklists, communityOfficial, accuratePaid for commercial use
Antispam BeeFreeHoneypot, geolocationFree, GDPR compliantFewer features
CleanTalk$8/yrAI, behavior analysisAdvanced featuresDepends on external API
WP ArmourFreereCAPTCHA, honeypotSimpleBasic feature set

Custom anti-spam solutions

Honeypot implementation:

add_action('comment_form_after_fields', 'add_honeypot_field');
function add_honeypot_field() {
    echo '<p style="display:none;">
        <label for="url-extra">Leave this field empty:</label>
        <input type="text" name="url-extra" id="url-extra" value="" />
    </p>';
}

// Check honeypot on submit
add_action('pre_comment_on_post', 'check_honeypot');
function check_honeypot() {
    if (!empty($_POST['url-extra'])) {
        wp_die('Spam detected.', 'Error', ['response' => 403]);
    }
}

Behavioral analysis:

// Analyze form completion time
add_action('comment_form', 'add_timing_check');
function add_timing_check() {
    echo '<input type="hidden" name="comment_timestamp" value="' . time() . '" />';
}

add_filter('pre_comment_approved', 'timing_spam_check', 99, 2);
function timing_spam_check($approved, $commentdata) {
    $timestamp = $_POST['comment_timestamp'] ?? 0;
    $time_spent = time() - $timestamp;
    
    // Too fast (bot) or too slow (abandoned form)
    if ($time_spent < 5 || $time_spent > 3600) {
        return 'spam';
    }
    
    return $approved;
}

CAPTCHA implementation

Google reCAPTCHA v3:

// Add reCAPTCHA to the comment form
add_action('comment_form_after_fields', 'add_recaptcha_v3');
function add_recaptcha_v3() {
    $site_key = 'YOUR_RECAPTCHA_SITE_KEY';
    
    echo '<script src="https://www.google.com/recaptcha/api.js?render=' . $site_key . '"></script>';
    echo '<script>
        grecaptcha.ready(function() {
            grecaptcha.execute("' . $site_key . '", {action: "comment"}).then(function(token) {
                document.getElementById("g-recaptcha-response").value = token;
            });
        });
    </script>';
    echo '<input type="hidden" id="g-recaptcha-response" name="g-recaptcha-response" />';
}

// Validate reCAPTCHA
add_filter('pre_comment_approved', 'validate_recaptcha', 10, 2);
function validate_recaptcha($approved, $commentdata) {
    $secret_key = 'YOUR_RECAPTCHA_SECRET_KEY';
    $response = $_POST['g-recaptcha-response'] ?? '';
    
    if (empty($response)) {
        return new WP_Error('recaptcha_required', 'reCAPTCHA verification required.');
    }
    
    $verify = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', [
        'body' => [
            'secret' => $secret_key,
            'response' => $response,
            'remoteip' => $_SERVER['REMOTE_ADDR']
        ]
    ]);
    
    $verify_body = wp_remote_retrieve_body($verify);
    $result = json_decode($verify_body, true);
    
    if (!$result['success'] || $result['score'] < 0.5) {
        return 'spam';
    }
    
    return $approved;
}

Cloudflare Turnstile (reCAPTCHA alternative):

// Cloudflare Turnstile implementation
add_action('comment_form_after_fields', 'add_turnstile');
function add_turnstile() {
    echo '<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>';
    echo '<div class="cf-turnstile" data-sitekey="YOUR_TURNSTILE_SITE_KEY"></div>';
}

add_filter('pre_comment_approved', 'validate_turnstile', 10, 2);
function validate_turnstile($approved, $commentdata) {
    $secret = 'YOUR_TURNSTILE_SECRET_KEY';
    $token = $_POST['cf-turnstile-response'] ?? '';
    
    if (empty($token)) {
        return new WP_Error('turnstile_required', 'Turnstile verification required.');
    }
    
    $response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
        'body' => [
            'secret' => $secret,
            'response' => $token,
            'remoteip' => $_SERVER['REMOTE_ADDR']
        ]
    ]);
    
    $body = json_decode(wp_remote_retrieve_body($response), true);
    
    if (!$body['success']) {
        return 'spam';
    }
    
    return $approved;
}

4. Monitoring and forensic analysis

Logging spam attempts

// Comprehensive logging of comment activity
add_action('wp_insert_comment', 'log_comment_activity', 10, 2);
function log_comment_activity($id, $comment) {
    $log_data = [
        'comment_id' => $id,
        'post_id' => $comment->comment_post_ID,
        'author_ip' => $comment->comment_author_IP,
        'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
        'referer' => $_SERVER['HTTP_REFERER'] ?? '',
        'timestamp' => current_time('mysql'),
        'status' => $comment->comment_approved
    ];
    
    // Store to a custom log table or file
    error_log('COMMENT_LOG: ' . json_encode($log_data));
    
    // Detect suspicious patterns
    if (is_comment_suspicious($comment)) {
        // Send an alert to administrators
        wp_mail(
            get_option('admin_email'),
            'Suspicious comment detected',
            sprintf('Suspicious comment detected from IP %s on post %d', 
                $comment->comment_author_IP, 
                $comment->comment_post_ID
            )
        );
    }
}

function is_comment_suspicious($comment) {
    // Check for known spam patterns
    $spam_indicators = [
        strlen($comment->comment_content) < 5, // Příliš krátký
        preg_match('/https?:\/\//', $comment->comment_content) > 2, // Mnoho linků
        preg_match('/\[url=|\[link=/', $comment->comment_content), // BBcode linky
        empty($comment->comment_author_email), // Bez emailu
    ];
    
    return array_sum($spam_indicators) >= 2;
}

Database analysis of spam

// Function to analyze spam comments
function analyze_comment_patterns() {
    global $wpdb;
    
    // Top IPs with spam comments
    $spam_ips = $wpdb->get_results("
        SELECT comment_author_IP, COUNT(*) as count 
        FROM {$wpdb->comments} 
        WHERE comment_approved = 'spam' 
        AND comment_date > DATE_SUB(NOW(), INTERVAL 30 DAY)
        GROUP BY comment_author_IP 
        ORDER BY count DESC 
        LIMIT 20
    ");
    
    // Temporal attack patterns
    $time_patterns = $wpdb->get_results("
        SELECT HOUR(comment_date) as hour, COUNT(*) as count
        FROM {$wpdb->comments} 
        WHERE comment_approved = 'spam'
        AND comment_date > DATE_SUB(NOW(), INTERVAL 7 DAY)
        GROUP BY HOUR(comment_date)
        ORDER BY count DESC
    ");
    
    return [
        'spam_ips' => $spam_ips,
        'time_patterns' => $time_patterns
    ];
}

5. Completely disable comments

Plugin Disable Comments

The easiest way is to use the Disable Comments plugin:

Features:

  • Completely remove the comments UI
  • Block all comment endpoints
  • Clean up existing comments
  • Support for custom post types

Manual disable

// Completely remove the comment system
add_action('admin_init', 'disable_comments_admin');
function disable_comments_admin() {
    // Remove menu items
    remove_menu_page('edit-comments.php');
    remove_submenu_page('options-general.php', 'options-discussion.php');
}

add_action('init', 'disable_comments_frontend');
function disable_comments_frontend() {
    // Remove comment support from all post types
    $post_types = get_post_types();
    foreach ($post_types as $post_type) {
        if (post_type_supports($post_type, 'comments')) {
            remove_post_type_support($post_type, 'comments');
            remove_post_type_support($post_type, 'trackbacks');
        }
    }
}

// Block wp-comments-post.php
add_action('wp_loaded', 'block_comments_completely');
function block_comments_completely() {
    if (strpos($_SERVER['REQUEST_URI'], 'wp-comments-post.php') !== false) {
        wp_die('Comments are completely disabled.', 'Comments Disabled', 410);
    }
}

// Remove from feeds
add_filter('comments_open', '__return_false', 20, 2);
add_filter('pings_open', '__return_false', 20, 2);
add_filter('comments_array', '__return_empty_array', 10, 2);

6. Performance optimization

Database optimizations

-- Clean up spam comments older than 30 days
DELETE FROM wp_comments 
WHERE comment_approved = 'spam' 
AND comment_date < DATE_SUB(NOW(), INTERVAL 30 DAY);

-- Optimize the comment meta table
OPTIMIZE TABLE wp_commentmeta;

-- Index for faster queries
CREATE INDEX idx_comment_approved_date ON wp_comments(comment_approved, comment_date);

Caching considerations

// Invalidate cache on new comments
add_action('comment_post', 'clear_comment_cache');
function clear_comment_cache($comment_id) {
    $comment = get_comment($comment_id);
    
    // Purge page cache
    if (function_exists('wp_cache_flush')) {
        wp_cache_flush();
    }
    
    // LiteSpeed Cache
    if (class_exists('LiteSpeed_Cache_API')) {
        LiteSpeed_Cache_API::purge_post($comment->comment_post_ID);
    }
    
    // W3 Total Cache
    if (function_exists('w3tc_flush_post')) {
        w3tc_flush_post($comment->comment_post_ID);
    }
}

7. Security monitoring

Server-level monitoring

.htaccess advanced rules:

<IfModule mod_security.c>
    # Block common spam patterns in comments
    SecRule ARGS:comment "@detectSQLi" \
        "id:1001,phase:2,block,msg:'SQL Injection in comment'"
    
    SecRule ARGS:comment "@detectXSS" \
        "id:1002,phase:2,block,msg:'XSS in comment'"
    
    # Rate limiting pro comment submissions
    SecRule IP:comment_rate "@gt 5" \
        "id:1003,phase:2,deny,status:429"
    
    SecAction "id:1004,phase:2,setvar:IP.comment_rate=+1,expirevar:IP.comment_rate=60"
</IfModule>

Fail2Ban configuration

# /etc/fail2ban/jail.local

[wordpress-comment-spam]

enabled = true filter = wordpress-comment-spam action = iptables-multiport[name=wp-comment, port=”http,https”] logpath = /var/log/apache2/access.log maxretry = 5 bantime = 3600 findtime = 300 # Filter: /etc/fail2ban/filter.d/wordpress-comment-spam.conf [Definition] failregex = ^<HOST> .* “POST .*wp-comments-post\.php.*” 403 ^<HOST> .* “POST .*/wp-json/wp/v2/comments.*” 40[13]

Security checklist

Basic protection (must-have)

  • [ ] Comments only for logged-in users
  • [ ] Disable pingbacks and trackbacks
  • [ ] XML-RPC disabled or restricted
  • [ ] Anti-spam plugin (Akismet/Antispam Bee)
  • [ ] Automatically close old comments
  • [ ] Moderate new comments

Advanced protection (recommended)

  • [ ] Secure REST API endpoints
  • [ ] Protect wp-comments-post.php
  • [ ] Implement CAPTCHA/Turnstile
  • [ ] Rate limiting configured
  • [ ] Add honeypot field(s)
  • [ ] Enable behavioral analysis

Expert level (optional)

  • [ ] Server-level filtering (.htaccess/Nginx)
  • [ ] Fail2Ban configuration
  • [ ] Custom logging implemented
  • [ ] Database optimization done
  • [ ] Performance monitoring configured
  • [ ] Security headers configured

Conclusion & recommendations

Securing WordPress comments requires a multi-layered approach that combines proper admin configuration, technical defenses, and active monitoring. The most effective path is to start with the basics and then add advanced measures as your site’s needs grow.

Key takeaways:

  1. Prevention over cleanup — it’s better to stop spam than remove it later
  2. Layered security — combine multiple defenses
  3. Regular monitoring — actively track and evaluate attacks
  4. Performance impact — always weigh the effect on site speed

For most sites, a combination of proper admin settings, a quality anti-spam plugin, and basic technical hardening is enough. Advanced methods are suitable for high-traffic sites or those with specific security requirements.

Useful external resources:

BFCache in WordPress: Instant Navigation and the No-cache BFCache Plugin
BFCache in WordPress: Instant Navigation and the No-cache BFCache Plugin
07 Aug, 2025

Looking for something?