Most attention goes to the contact form — the REST API, the POSTs that slip past the CDN. While studying that, we kept running into a second vector that is arguably worse, and it is baked into WordPress core. No plugin required. It affects essentially every WordPress site on the planet. It is the search box.
How WordPress search works
When a visitor searches, the URL becomes /?s=keyword and WordPress runs a query like this against the posts table:
SELECT * FROM wp_posts
WHERE post_status = 'publish'
AND (post_title LIKE '%keyword%'
OR post_content LIKE '%keyword%')
A LIKE '%keyword%' pattern with a leading wildcard is one of the most expensive things you can ask SQL to do. The leading % means the index is useless, so MySQL performs a full table scan — every row read and compared. On a site with thousands of posts, a common word matches almost everything, and WordPress then renders a full paginated HTML results page: tens to hundreds of kilobytes out, for a request of about 167 bytes in.
The cache cannot save you
CDNs do not cache URLs with query strings by default — search responses come back cf-cache-status: BYPASS. Even if you force a cache rule, an attacker defeats it by appending a random suffix so every URL is unique:
/?s=the482910
/?s=the839201
/?s=the114857
Each is a brand-new cache key the edge has never seen, so every request is a guaranteed miss that hits the origin. The mechanics of HTTP caching make this unavoidable: a unique key cannot be a cache hit.
Double exhaustion: workers and the database
The contact-form attack exhausts PHP workers. Search exhausts both workers and MySQL at once. Each request occupies a worker from first byte to last (longer than a JSON form response, because it renders a whole page), holds a database connection for the duration of the scan, and spikes database CPU. When the MySQL connection pool runs dry, every database operation on the site fails — sessions, post lookups, options — not just search. The site collapses at the data layer, which is a lower effective threshold than worker exhaustion alone.
Short, common words are the worst case: they match nearly every row, so the scan and the render are maximally expensive. A nonsense string matches nothing and costs almost nothing. Defenders should assume the worst case, not the average.
What we measured
| Metric | Value |
|---|---|
| Attack request size | ~167 bytes |
| Search response size | ~37,800 bytes |
| Average response time under flood | 4.95s (vs 0.76s baseline) |
| Peak response time observed | 17.09s |
| Cache status | BYPASS on every request |
A peak near 17 seconds means MySQL was grinding a single query for most of that time while the worker sat occupied. Multiply by concurrency and the degradation is obvious.
The fix
Pick the option that matches your needs. If you do not need on-site search, redirect it away so no worker or query is ever spent:
function disable_wp_search( $query ) {
if ( $query->is_search() && ! is_admin() ) {
wp_redirect( home_url(), 301 );
exit;
}
}
add_action( 'pre_get_posts', 'disable_wp_search' );
If you do need it, rate-limit GET requests carrying s= at the edge (around 10 per 30 seconds per IP), or move search off your stack entirely onto a dedicated index like Elasticsearch or Algolia so your workers are never the thing doing the scan. That last option is the one we usually recommend for content-heavy sites — it is the same instinct behind keeping our own blog search client-side.
Two vectors, one problem
Search and the contact form are the same flaw wearing different clothes: an unauthenticated endpoint, uncacheable, triggering expensive work, with no default rate limit. We covered the form side in the Cloudflare myth and 167 bytes; the complete set of defenses lives in the hardening guide. If you want a second pair of eyes on your own attack surface, that is what our cyber defense team does.
All research conducted on authorized test infrastructure. Only test systems you own or have explicit written permission to test.
Discussion 0
Sign in or create a free account to comment and vote.
No comments yet. Be the first to share your thoughts.