Comment spam is a security problem, not an inbox problem
Comment forms are the most probed input on a WordPress site, and the spam is the visible fraction. What the traffic is doing and what each filter costs.
Tomas Lindqvist · @tlindqvist
Hardening and operations
- Topic
- SEC
- Section
- Security & Hardening
- Published
- 17 OCT 2026
- Reading time
- 9 MIN
- Words
- 2094W
Somebody finally opened the moderation queue on a site that had been live for four years. It held a little over eight thousand pending comments. Six were real: a delivery that had not arrived, two people asking whether a discontinued product would return, a job enquiry, and a customer who wrote three paragraphs about a broken order and never came back. The queue was not the problem. It was the only copy of those six comments, and nobody had seen them.
The site had comments open on pages as well as posts, because the theme printed the form wherever the content area ended. So the spam landed on the contact page, under a contact form that did have a notification email attached. Two ways to write to the company existed. One was read.
The shape repeats. The visible symptom is a spam folder with thousands of items. The problems are an input nobody owns, a queue nobody reads, and a site that keeps accepting text from strangers in a place its owner forgot. Spam announces itself. It is not the part of the traffic that does the damage.
Disclosure: some links on this page are affiliate links, and WPSifero may earn a commission if you buy through them. That does not change the price you pay, or what this piece recommends: the free options are named next to the paid one, and a later section is about when not to buy.
What the spam is actually doing
A comment form on a stock WordPress site is two public endpoints. One is wp-comments-post.php, which takes an unauthenticated POST and writes a row to the comments table. The other is the REST route under /wp-json/wp/v2/comments, which does the same with a JSON body. Neither wants an account or a token. On a site with a contact form, a search box, a login screen and a checkout, the comment form is usually the only input that will store arbitrary text from a stranger without a payment.
Link injection is the obvious payload: a body that reads like somebody who found the article useful, and a URL that sells something else. Approved, the link sits on your domain and inherits whatever authority the site has built. That is the whole product. A link in a spam folder is worth less, but not nothing: themes that print "awaiting moderation" beside the submitted URL leak it anyway.
The second kind does not care about being published. It checks whether the endpoint exists, how fast it answers, and with what. A site that says "comments are closed" behaves differently from one that says "duplicate comment detected", and differently again from one that redirects to a thank-you page. That difference is the fingerprint.
The third kind matters more than it looks: spam tests whether anybody is home. A comment that appears within a week tells the operator this site has a heartbeat. The address goes back into a list and the next round arrives with a better payload, because the first one worked. Approving spam is not a neutral act costing one link to a pharmacy. It is a reply.
None of this is visible from the moderation screen. Requests stopped by a security plugin, empty POSTs, probes that quit after one response: none of it reaches the queue. The access log has it. Counting POSTs to those paths over a week is the only honest measure of how hard the form is hit, and it is usually a different order of magnitude from the queue.
Turning comments off is a decision about the product
The fastest fix is to decide the form should not exist, and for a lot of sites that is correct.
What matters is whether comments are part of the product or an ornament the theme added. On a publication with a comment culture, a course site whose lessons have threads, or a listings site where buyers ask sellers questions in public, comments are something the business sells. On a five page company site, a restaurant page with a menu and hours, a portfolio, a plugin's documentation, they are furniture. Nobody has chosen a plumber because of the thread under the About page.
Two mechanics matter. The Discussion setting that turns comments off applies to new posts only; existing posts keep their forms until they are closed in bulk or by the old-post rule. And closing comments does not touch pingbacks, which have a separate switch and their own endpoint in wp-trackback.php. Leave that on and a site with comments closed still accepts POSTs from anyone, which is the failure the exercise was meant to end.
Closing is wrong where the answer is the product. A business that routes support through a documentation thread, or a membership site where the discussion under a lesson is why people renew, cannot close the form, and no filter replaces the person who works that queue.
Four filters and what each one costs in false positives
A blocklist is the cheapest thing that counts as filtering. WordPress ships two lists under Settings, Discussion: keys that hold a comment for review, and disallowed keys that send it to spam. It fails quietly. A blocklist is judged by the words it does not contain, so the ones it does contain are the ones that annoyed somebody once: a surname holding a matched substring, a city, a clinic with a medical domain, a comment in a script the list treats as noise. Address blocks go stale faster, and a blocked comment gets no bounce, so nobody records it.
A honeypot and a clock are the cheapest thing that is not a blocklist. Add a field a browser will not fill, hide it, reject submissions that fill it, and reject anything typed faster than a person could manage. Nothing judges the wording, so this is the only option here with essentially no false positives on language. The costs hide in the implementation: a field hidden only with CSS gets tabbed into by keyboard users, and a bot driving a headless browser ignores the trick.
A filter that learns from your own traffic runs on your server, and free plugins do this. Antispam Bee is the one most sites end up with: local pattern checks, comment age, the country of the request, and a corpus that is yours rather than shared. Nothing leaves the site, so there is no key, no account and no fee, which matters when the privacy policy promises that. The cost is volume. A site getting a dozen spam comments a month gives it nothing to learn from, and it will hold the odd real comment while it waits for a pattern.
A hosted service with a corpus that spans sites is the other end. Akismet is the comment spam filter that ships with WordPress, the one most sites install without choosing it. What it buys is knowledge of traffic that has not reached you yet, the one thing a local filter cannot have. What it costs is a dependency and a data flow: author name, email, address and comment body go out to be classified, which belongs in the privacy policy and may be a problem under stricter readings of data protection rules. Its false positives cluster: the short genuine comment, the comment with a code sample, the one with three links to the site's own pages, the one in a second language.
The filter that drops a real comment and tells nobody
The mechanisms are not equally visible. The good one sets the status to spam, where it waits until somebody rescues or deletes it. The acceptable one sets it to pending, where it joins the queue that is supposed to be read. The bad ones reject at insert with an error the commenter can see, or delete the row before WordPress finishes writing it. On that last path the comment never exists, the commenter sees a page that looks like success, and no record says otherwise.
Whether a filter does this is not documented consistently and can change between releases, so test it. The test is not whether spam gets through. It is where a real comment lands.
- Read the spam folder before emptying it, weekly, for a month after any change. Keep a count, from
wp comment list --status=spam --format=countor the dashboard, and write it down. A spam count that falls to zero is a signature of misconfiguration more often than of a solved problem. - Post test comments that look real: on another network, one with a link to your own domain, one short enough to look like spam, one in the audience's second language. Note where each lands, and post them signed out, because comments from an account that can moderate skip the filter.
- On a staging copy, hook
pre_comment_approvedand log every decision. A status ofspamshows up. Silence is the thing to look for, and the only notice of a total silent drop is a person saying the comment never appeared.
So prefer a filter that sets a status over one that discards. A folder is a decision somebody can reverse in a minute. A dropped comment is a person who left without saying so.
When the free option is genuinely enough
A blog with a few dozen comments a month, one language, threads worth reading, and one person who opens the queue on Fridays does not need a subscription. A honeypot, the two native key lists, the setting that holds first-time commenters, and a free server side plugin cover it. The spam that survives is repetitive, which a local list handles well, and a weekly pass takes ten minutes.
Paying starts to make sense when the shape changes: the form sits in the sales path, volume is high, the audience writes in more than one language, nobody is available to tune a list, or campaigns got through a local filter before it caught up. That is where a shared corpus earns its keep, and Akismet's comment and form spam filtering is what most WordPress sites end up running. Check the current price before deciding, since tiers have moved and the free one is aimed at personal sites.
The free route costs attention: ten minutes a week, a monthly count, a habit of reading a folder. A site where nobody will spend that is running no filter and calling it one.
Where paying is the wrong answer
Buying a filter when nobody reads the queue moves spam from pending to a folder. Classification improves, the outcome does not, because the missing piece was triage.
It is also the wrong purchase when the traffic is not comment spam. Registration spam, junk through a custom form, password guessing over XML-RPC and fake orders are separate pipelines, and a comment filter watches none of them. Count the access log for a week before paying: a site behind a proxy that blocks the endpoint shows a quiet queue while the form is still being hammered, and classifying comments that are already blocked upstream is paying twice for one job.
Privacy requirements can rule the hosted option out entirely, which is not a quality judgement. If comment bodies and addresses have to stay on infrastructure the operator controls, a service that classifies them elsewhere is off the table, and the local filter is the answer even when it misses more.
Finally, no product rescues the twenty real messages buried in a queue of thousands. A filter switched on today does not go back and sort four years of pending comments. That is an afternoon with a search box, and it is the most valuable part of the whole job.
The decision rule
Work in order. Decide whether the form should exist on this page, and remove it where it should not. Close comments where the product does not need them, pingbacks included, and print one address that gets read. Where the form stays, add the honeypot and the native rules. Measure for a month: POSTs in the access log against comments in the queue. Then pick a filter, prefer one that sets a status, and read that status for four weeks. Pay for what cannot be done in house, and check the current price rather than a figure from a review written two years ago.
The test of a working filter is not an empty queue. It is whether the operator can name the last three comments it caught and the last one it got wrong. A filter nobody can audit is not a filter. It is a lid.
Tomas Lindqvist · @tlindqvist
Hardening and operations
Threat models plugins, audits capability and nonce handling, and builds the observability that tells you an integration broke before a customer does.
Continue