“Auto-Updates: On” Is Not the Same as Patched

A business called us: their WordPress admin password had been changed, and they hadn’t done it. What we found was a two-week-old compromise, traced to a single security update that a hosting setting had quietly blocked from ever installing. This is what we found, and how it happened.

By Nathan Cross, Co-Founder, Network & Security Engineering·

14 days
of quiet attacker access before anyone noticed
Same day
fully contained and eradicated once we were called in
0
customer records exposed; those live on a separate platform

It started with an email nobody wants to get. The owner opened their inbox to a notice from their own website: the administrator password had just been changed. They hadn’t changed it. Someone else had.

By the time that email landed, the attacker had already been living on the server for two weeks. They reached out to us, and we treated it as what it turned out to be: an active, ongoing compromise.

The way in was a critical vulnerability in WordPress core, not a plugin: the pre-authentication chain publicly nicknamed “wp2shell” (CVE-2026-63030 and CVE-2026-60137). It was disclosed and fixed in WordPress 7.0.2 on July 17, 2026. This site was still on 7.0.1, and it was broken into on July 19, two days later. Automated scanners were hunting for unpatched installs within hours of the fix going public.

The attack chain

From one anonymous request to total control

No password, no phishing, no vulnerable plugin. A single unauthenticated HTTP request walked the whole way down this chain.

  • 1
    One anonymous HTTP request
    no login · no plugin · no warning
  • 2
    Reaches the REST batch endpoint
    route confusion · CVE-2026-63030 · /wp-json/batch/v1
  • 3
    Slips into a SQL injection
    CVE-2026-60137
  • 4
    Runs code on the server
    full pre-authentication RCE
  • 5
    Plants a web shell and a hidden admin
    persistence, so a password reset alone won't help
  • !
    Runs a credential-harvesting payload
    total control of the site and the server
First move, before anything else

We took a full live copy of the site before touching a thing. In an active compromise the site is the crime scene: patch a file or delete a shell too early and you destroy the evidence you need to understand what happened, and to prove it is really gone. The copy let us triage the malware safely, offline and afterward, while the cleanup went ahead in parallel.

// how it unfolded
  • Jul 17, 2026
    WordPress ships 7.0.2

    The fix for the core flaw is public. On a healthy site, it installs on its own within hours.

  • Jul 19 · 2 days later
    The site is breached

    Still on 7.0.1, a single anonymous request exploits the core flaw and plants a foothold: a hidden administrator account and a web shell.

  • Jul 19 – Aug 2
    ~two weeks of quiet access

    The attacker comes and goes through the web shell. No defacement, no visitor-facing malware, nothing a casual look would reveal.

  • Aug 2
    The attacker escalates, and slips

    They drop a data-harvesting tool and run it, add a second hidden admin, and change the owner’s password, which fires the email that finally gives them away.

  • Aug 2 · called in
    Copy first, then contain

    We take a full forensic copy of the live site, then remove every web shell and rogue account and lock the attacker out.

  • Aug 2 · same day
    Patch, trace, harden

    We apply the missing update by hand, track down the point of entry, fix why the patch never installed, and rotate every secret.

  • Verified
    Clean, inside and out

    Confirmed no attacker access remains, and that security updates will now actually install.

Why the patch was missing

The exploit is one thing. The reason this site was still exposed two days, and then two weeks, after a fix existed is the part worth remembering. The site looked completely healthy from every angle a person normally checks. Its WordPress admin showed automatic updates enabled. WordPress’s own Site Health tool reported a clean bill: “Background updates are working.” By every standard signal, this site was keeping itself current.

None of it was true. The hosting provider had layered its own site-management tooling on top of WordPress, and that tooling quietly installed a component that switched off core auto-updates at a priority almost nothing can override, on the assumption that the provider would apply updates itself instead. It didn’t. So the security release never installed, while the dashboard and Site Health both kept reporting green.

mu-plugins/host-update-manager.php
// Turn off WordPress automatic updates (handled externally by the host).
add_filter( 'auto_update_core',              '__return_false', -9999 );
add_filter( 'allow_minor_auto_core_updates', '__return_false', -9999 ); // security releases
add_filter( 'allow_major_auto_core_updates', '__return_false', -9999 );
The priority -9999 is the tell: set low enough that almost nothing loaded afterward can turn updates back on.

✓ The dashboard said

  • ✓Core auto-updates: enabled
  • ✓Site Health → Background updates: good
  • ✓“WordPress can auto-update if a security update is released.”
STATUS: all green · keeping itself current

✗ Reality was

  • ✗Effective auto_update_enabled('core'): false
  • ✗Still running the pre-patch, exploitable version
  • ✗Compromised, with an attacker on the server
STATUS: unpatched · breached

The Site Health test keys off the update infrastructure, not the one filter that had been flipped, so it read good on the day of the breach. The green status was describing the intent, not the reality.

What it was really after

Then we pulled apart what the attacker left behind, and it stopped looking like an ordinary hack. The main tool wasn’t a crude web shell. It was a 30-kilobyte, purpose-built harvesting engine, and reading it is like reading a shopping list for the modern internet. We recovered it intact and analyzed it offline, without ever running it.

The first thing it reached for was every API key it could find. Its built-in hit-list targets exactly the credentials that are worth real money today:

OpenAIAnthropicGooglexAIHuggingFaceStripeAWSPayPalTwilioSendGridMailgunand more
Steal

Every secret in the database and the config

The entire wp-config.php (database password and all secret keys), the full options table, then pattern-matched sweeps across every metadata table for anything shaped like a key (a Stripe sk_live, an sk- AI token, a PEM private key), and a filesystem hunt for .env files, backups, and database dumps.

Root

Try to own the whole server

It fingerprinted the machine and checked it against eleven separate Linux privilege-escalation exploits, computing which one had the best odds of turning a hacked website into full control of the server it runs on.

Spread

Pivot to the neighbours

On shared hosting you have neighbours. The tool went looking for them: other sites on the same box, their config files, and any SSH keys it could steal to jump from one victim to the next.

How close this was

Here, it came up mostly empty. This particular site held no API keys, no .env file, and the shared hosting boxed in its attempts to escalate, so the damage stayed contained to the site itself. But read that back: the only reason a full-scale credential theft didn’t happen is that this business happened to store none of the things the tool hunts for. On a site that kept a Stripe key, an OpenAI key, or an SSH key sitting in its config, the exact same automated payload would have walked away with all of it, in the same fourteen quiet days. That is luck, not a defense.

// removed, then verified gone
  • ✗
    Two web shellsHidden inside a fake “optimizer” plugin, each able to run arbitrary commands on the server.
  • ✗
    Two hidden administrator accountsPlanted so the attacker keeps a way back in even if one door closes.
  • ✗
    A forged admin session and the harvesting toolSession killed, tool removed, and every secret it could have read rotated on the assumption it was taken.
  • ✓
    Core updated, entry point closed, site hardenedThe missing security release applied by hand, and the disabled updater fixed so a patch can’t be silently skipped again.
A setting is a promise.
The version number is a fact.
the whole incident, in one line

Fixing the cause, not just the symptom

Pulling the shells and the rogue accounts closes the incident, but it doesn’t stop the next one. The part that actually prevents a repeat was tracing the breach to its real root cause, the silently disabled updater, and fixing that too. We added a small override that re-enables verified security auto-updates and outranks the block, so fixes now install on their own. Major version jumps stay manual on purpose, since those can break a design and deserve a human. Then we verified the site clean from the inside and the outside, and confirmed that a security release will now actually apply.

The lesson: verify the patch, not the promise

This site passed every check that most people, and most tools, rely on. The dashboard said updates were on. Site Health said they were working. Both reported healthy right up to the day the attacker announced themselves by changing the password. The one thing that would have caught it earlier was going a layer deeper than any of those signals: comparing the site’s actual running version against the current release, and treating any gap as a problem regardless of what the settings claim.

That’s the difference between believing a promise and verifying a fact. When a provider, a plugin, or a misconfiguration silently breaks the link between “updates: on” and “updates applied,” only the version number will tell you.

Three checks worth doing today
  1. 01Find the version your site is actually running and compare it to the latest release. Not “updates: on”, the real version number.
  2. 02Confirm the last security release actually installed, rather than trusting that auto-updates are “enabled.” If your host manages updates, ask them to prove the last one landed.
  3. 03Scan from the outside. An external scan reads the version you are really serving and checks it against known vulnerabilities, no dashboard required.
How we check for this now

On every site we work on, we confirm the version it is actually running against the current release, independent of what its dashboard claims. Our own scanner, BadgerScan, does exactly that: it reads the live version, from the outside where it is exposed and from the inside with its plugin, and flags an outdated, exploitable core as a real finding no matter what a setting says.

Not sure your updates are actually applying?

Run a free scan and see the exact version your site is running, checked against known vulnerabilities. It reads the fact, not the dashboard’s promise, which is the whole gap this incident came down to.

Run a free security scan

Frequently asked questions

Does “auto-updates: on” mean my WordPress site is patched?

Not necessarily. “Auto-updates enabled” is a setting, a promise about what should happen. A hosting layer, a plugin, or a misconfiguration can silently block updates from actually installing while the dashboard still reports them as on. The only reliable check is the version the site is actually running, compared against the current release.

How was the site hacked if the password was not guessed?

It was not a password attack. The entry point was a pre-authentication flaw in WordPress core (the “wp2shell” chain, CVE-2026-63030 and CVE-2026-60137), fixed in WordPress 7.0.2. A single anonymous request could run code on an unpatched site, with no login required. This site was still on 7.0.1 because a hosting setting had blocked the update from installing.

Were customer records exposed?

No. The business’s customer records live on a separate platform that was never touched. Exposure was limited to website infrastructure credentials and WordPress account data, and the most sensitive of those, the database password, is not reachable from the public internet.

Keep reading

Sources

  1. WordPress 7.0.2 Security Release (WordPress.org News)
  2. CVE-2026-63030 (National Vulnerability Database)
  3. CVE-2026-60137 (National Vulnerability Database)

More from CyberBadger

BadgerScan is the website side of what we do. We're one local Hamilton and Burlington team for your whole setup, on-site nearby and remote across Canada.

Coming soon: BadgerAudit. A full, on-site cybersecurity audit, interviews, hands-on review, and a detailed report, for when a self-serve scan isn't enough. Ask us about it.