DEV Community

Cover image for Your site search box is a public API, and bots know it
Rafał Groń
Rafał Groń

Posted on • Originally published at queryra.com

Your site search box is a public API, and bots know it

I run a search API for WordPress and WooCommerce stores. Between 31 March and 20 July 2026 it logged 155,730 queries that contained actual text.

Under 3% came from a real store.

I want to walk through what the other 97% actually was, what it cost me to not filter it, and the heuristics that turned out to work. If you have ever built anything on top of user-submitted text, most of this transfers.

The endpoint nobody thinks of as an endpoint

On WordPress, ?s= is an unauthenticated, unrate-limited, GET-based text input that accepts arbitrary strings and renders them into a page. Every install has one. It is reachable whether or not the theme renders a search form anywhere.

One site in my data had no visible search field at all and was still collecting queries.

Treat it as what it is: a public API you did not design, did not document, and are not monitoring.

What actually arrives there

Bare numbers. A steady trickle of queries that are nothing but a number, usually a year. All return zero. This puzzled me for weeks until the explanation turned out to be mundane: spam networks stand up throwaway pages and use a site's own search to check whether those pages still resolve.

Volume that dwarfs everything real. A single source sent roughly 65,000 queries in the period. That is about fourteen times the entire genuine shopper traffic across every connected store combined.

Exploit probes. Injection strings and framework attack paths, arriving through search because it is the easiest unauthenticated text field on the platform.

Advertising. This one is my favourite, because you can see the whole attack in two log lines. First the reconnaissance:

query: "serpdummycrawl1"
Enter fullscreen mode Exit fullscreen mode

Someone checking whether search result pages are crawlable and indexable. Then, on a different site, the payload:

query: "[ NOTHING done overseas ]  TX based  27 Years Experience
        Only $199/mo  ...  https://adventdigital.net/seo/17-point-plan/
        ...  (Unsubscribe on Request)"
Enter fullscreen mode Exit fullscreen mode

A full advertisement pasted into a search box. The goal is not that a human reads it. The goal is that the search results page gets indexed with their link on it.

The defence for that one is trivial and worth knowing: put noindex on search result pages. Yoast and RankMath do it by default. If your search pages are not indexable, the entire attack is pointless. Most sites already have this and have no idea it is doing anything.

What it costs to not filter

That advertisement was not free to process.

total: 2078ms
Enter fullscreen mode Exit fullscreen mode

Two seconds of my pipeline, at full per-query cost, spent evaluating how relevant a Texas SEO agency is to a store selling air purifiers. Every year-string, every probe, every piece of filler got the same treatment.

The economics of this are worth stating plainly, because they surprised me: the cost of running my infrastructure is driven by abuse, not by customers. One abusive source cost roughly fourteen times what my entire real customer base cost over the same period. Rate limiting is worth more than any pricing tier I could design.

The heuristics that work

In rough order of value per line of code.

Rate limit per key, per window. This is the whole game. It requires zero classification. You do not need to know who is a bot, you only need to cap volume. It is also the only defence that survives an attacker changing their strings tomorrow.

Do not count filtered traffic against a customer's quota. I learned this the expensive way. One customer decided the product was too limited and left. Going through his log afterwards, a large share of his allowance had gone to traffic that was never going to buy anything. He was right that he ran out. He was wrong about who used it.

Short-circuit queries containing a URL. Nobody searches a product catalogue for https://. This is a one-line check that eliminates an entire class of pasted spam before it reaches anything expensive.

Filter on behaviour, not content. No referrer, no session, no subsequent page view. Identical strings arriving faster than a human types. These survive the attacker changing their payload. A blocklist of domains or strings does not.

Degrade, do not block. Return an empty result rather than a 403. A 403 tells the attacker they were detected and teaches them what to change. An empty result teaches them nothing.

The heuristic that looked obvious and was wrong

Blocking queries that are only digits.

It is tempting. Almost every one of them in my data was a spam probe returning zero. But I checked it against the whole fleet before shipping it, and found a content site where 2026 returns four legitimate results, because that site publishes articles with years in the titles.

One rule, one real customer, silently broken. Check your obvious heuristic against your entire corpus, not against the sample that suggested it.

And the trap on the other side

Filtering is only half of it. The half that got me was reading the filtered data.

I went through one store's zero-result list. The headline was that 48.9% of searches returned nothing, which looks like a five-alarm fire. After stripping the bot traffic, it was 15.3%. After that store finished importing its catalogue, it was under 7%.

Three numbers, same store, and only the last one was about customers.

That last step is the part I am least proud of. I found a query that came back empty on eight separate attempts and concluded the store simply did not sell that product. It did. I had not finished importing their catalogue. I was one step from writing that up as a customer insight when it was a gap in my own pipeline.

Before you conclude anything about a user from your data, check whether your own system was in a normal state when that data was produced. Sync state, feature flags, plan limits at the time rather than today. The most confident wrong answer I produced this year came from data I trusted because it was mine.

One finding that is not about bots

Since I was in there anyway.

Every demo of this kind of product shows a sentence. "Something for oily skin that isn't greasy." Those queries are real, I have them. But when I checked when they arrive, they cluster in the first day or two after a store connects, in a burst, from one location.

That is the shop owner, testing.

What real visitors type, weeks later, steadily, looks like this:

drifter salts
xros 5
epson emc800
Enter fullscreen mode Exit fullscreen mode

Names, model numbers, part codes, usually misspelled. Two populations, both real, wanting different things from the same input box. I had been building and marketing for the smaller one.


The longer version, with the store-owner side of this and a checklist for reading your own log, is on my blog: I Read 155,000 WooCommerce Searches So You Don't Have To

Top comments (0)