---
title: "The Scanner Said Clean. The Site Was Serving Spam to Googlebot."
url: https://adityaarsharma.com/the-scanner-said-clean-the-site-was-serving-spam-to-googlebot/
date: 2026-08-21
modified: 2026-08-21
author: "Aditya R Sharma"
description: "A client site was cloaking spam to search engines while every commercial security plugin reported no threats. Why signature scanners miss this, and the agent I built instead."
categories:
  - "AI"
  - "Cyber Security"
  - "WordPress"
word_count: 1062
---

# The Scanner Said Clean. The Site Was Serving Spam to Googlebot.

The site looked fine.

Loaded normally in a browser. Admin dashboard clean. Two commercial security plugins installed, both reporting no threats. The client had been paying for one of them for two years.

Search Console said otherwise. Pages indexed that did not exist. Japanese pharmaceutical keywords on a business services site.

That gap, between what the scanners saw and what Google saw, is the whole story. It is also the reason I ended up writing an agent instead of buying a tool.

## Why every scanner passed

Three reasons, and each one is a design decision rather than a bug.

### They fetch as themselves

The payload was cloaked. It checked the user agent and served spam only to Googlebot and a couple of other crawler signatures. A scanner requesting the page as itself gets the clean version, every time, correctly, forever.

### They scan wp-content and stop

That is where malware usually lives, so that is where they look. This one had a loader in a PHP file two directories above the web root, well outside the WordPress install. Not in `wp-content`. Not in core. Not scanned.

### They match signatures

The file was assembled at runtime from three fragments that individually look like ordinary configuration handling. No single fragment matches anything. The assembled behaviour is the payload, and it only exists in memory.

None of that is exotic. It is standard for anything written after about 2020. The scanners are not bad. They are answering a question that stopped being the right question.

## Finding it by hand

I found it by asking a different question: not "is there malware in the files" but "does this site serve different content to Googlebot than to me."

Fetched every URL twice, once as a browser and once as Googlebot, and diffed. Fourteen URLs came back different. All fourteen had injected link blocks visible only to the crawler.

From there it was tracing backwards. Which file writes that block, what includes that file, what wrote that file, and when. The entry point turned out to be an abandoned plugin with a known unauthenticated file upload vulnerability, still installed, still active, deactivated in the client's memory but not in the database.

Total time: most of a day. Most of it mechanical.

## Why an agent instead of a script

The mechanical parts were scriptable. The judgment parts were not.

"Is this obfuscated code malicious or just minified" is a judgment call. So is "does this file belong in this directory," and "is this modification to a core file a hack or a legitimate patch someone applied in 2019 and forgot."

A script has to encode those rules in advance. It cannot, so it either flags everything and you drown in noise, or it flags nothing and you are back where you started.

An agent can reason about it, and more importantly it can go and check. It can look at the file, look at when it was written, look at what else was written in the same minute, look at whether the plugin it belongs to is even installed, and then decide.

So I wrote it as an MCP server. Read-only by default. It reports and does not act unless you tell it to.

**What it does:**

- Fetches every page twice, as browser and as Googlebot, and diffs. This is the check that caught the original infection, and the one no commercial scanner performs
- Scans the entire server, not just `wp-content`
- Reads file modification times as a cluster, because an infection writes many files in one burst and that burst is the most legible signal on a compromised server
- Traces the entry point: which vulnerable component, which version, which known CVE
- Backs up before touching anything, with automatic rollback if a cleanup breaks the site

Open source and free. Thirty stars at the time of writing, which is small, and I would rather give the real number than gesture at traction.

## What broke in production

Three things, all of which taught me more than the parts that worked.

### False positives on legitimate obfuscation

Some commercial plugins ship minified and encoded code. It looks exactly like a payload. Early runs flagged a licensing module on every site that had it. The fix was checking file provenance, does this belong to an installed plugin at the expected path with a matching checksum, before judging the contents.

### Cleanup broke a site

A modified core file was hosting an injection. The agent removed the injection. The file also contained a legitimate patch the previous developer had applied directly to core, undocumented. Removing the injection removed the patch. Site broke. Rollback fired and restored it, which is the only reason this is an anecdote rather than an incident.

That is why the default is read-only now. **Detection and remediation are different risk profiles and should be different decisions.**

### Confident wrong answers on ambiguity

Asked whether a specific file was malicious, the agent would commit to a verdict with no hedging even when the evidence was genuinely thin. This is the failure mode that worries me most in agent tooling generally, because the confidence is indistinguishable from the confidence on cases it gets right.

The mitigation is structural rather than prompt-level: the tool returns evidence and a confidence band, not a verdict. The human decides. If the tooling wants to state a conclusion, it has to show what the conclusion rests on.

## The part that generalises

There is a lesson here that is not about malware.

The scanners were not broken. They were built to answer "are there known-bad files on this server," and they answer it well. The question that mattered was "does this server behave differently for different visitors," and nobody was asking it because it is expensive and stateful and does not fit a signature database.

Most of the tooling I have replaced with agents in the last year fits that shape. The old tool answers the tractable question correctly. The agent can afford to ask the expensive question, because asking it is now cheap.

That is the actual shift. Not that AI is smarter than the scanner. That a whole class of questions that were too costly to ask at scale are now trivial to ask.

If you run WordPress sites and have not checked what you serve to Googlebot lately, check. It takes a minute and the answer is occasionally unpleasant.