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 Type | Description | Severity |
---|---|---|
Performance | Database overload, site slowdown | High |
SEO | Spam links, SERP degradation | High |
Security | Potential XSS and injection attacks | Critical |
Costs | Higher server load, cleanup effort | Medium |
Reputation | Loss of visitor trust | Medium |
Comprehensive Security — Step by Step
1. Core settings in the admin
Discussion Settings (wp-admin/options-discussion.php)
Critical settings:
Settings → Discussion
Recommended configuration:
- Default post settings:
- Allow visitors to post comments on new articles
- Only registered and logged-in users can comment
- 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
- 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
- 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
Plugin | Price | Detection methods | Pros | Cons |
---|---|---|---|---|
Akismet | $5–50/mo | AI, blacklists, community | Official, accurate | Paid for commercial use |
Antispam Bee | Free | Honeypot, geolocation | Free, GDPR compliant | Fewer features |
CleanTalk | $8/yr | AI, behavior analysis | Advanced features | Depends on external API |
WP Armour | Free | reCAPTCHA, honeypot | Simple | Basic 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:
- Prevention over cleanup — it’s better to stop spam than remove it later
- Layered security — combine multiple defenses
- Regular monitoring — actively track and evaluate attacks
- 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: