A ticking bomb hidden in a trackback comment that only detonates when the site admin clicks 'Restore'
CVE-2026-19949 is a second-order SQL injection in the All-in-One WP Migration and Backup plugin for WordPress (all versions through 7.109, fixed in 7.110). The flaw lives in the plugin's replace_table_values() function, which rewrites URLs and table prefixes during an export/import cycle. A regex flaw in how backslashes and quoted strings are parsed lets attacker-controlled content — planted unauthenticated via WordPress trackbacks — escape its SQL string literal and execute as raw SQL. The injected query extracts the plugin's ai1wm_secret_key and writes it into a publicly visible comment, where the attacker retrieves it via the REST API. Armed with that key, the attacker imports a crafted .wpress archive containing a malicious must-use (mu) plugin, achieving full remote code execution. The plugin has 5 million+ active installs; as of early September 2026, only ~35% had updated, leaving an estimated 3.25 million sites on vulnerable versions.
The vendor rates this HIGH (8.8) with CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. That vector is misleading in two places. First, UI:N implies no user interaction is needed — but the entire chain is inert until a site administrator *actively performs an export-then-import*, a human action the attacker cannot force or schedule. Second, AC:L ignores the multi-phase, time-delayed nature of the attack: the attacker plants, waits (hours to months), and only gets a shot when the admin happens to restore. Wordfence themselves acknowledge this is a 'routine action' for the plugin's users, which keeps severity meaningful, but it is decidedly not zero-click. A more accurate vector would use AC:H/UI:R, which calculates to 7.5. The real-world severity is HIGH but closer to 7.2, not 8.8.
5 steps from start to impact.
Plant malicious trackback
blog_name or URL field contains a SQL injection payload — specifically, a carefully constructed string with mismatched backslashes designed to break out of a quoted literal during the plugin's find-and-replace pass. WordPress stores this trackback data in the wp_comments table as a comment of type trackback. No authentication is required; trackbacks are enabled by default in WordPress.- Target WordPress site has at least one published post
- Trackbacks/pingbacks are enabled (WordPress default)
- XML-RPC endpoint (
/xmlrpc.php) is reachable
- Many hardened WordPress deployments disable XML-RPC entirely (iThemes, Wordfence, Sucuri all recommend this)
- Some WAFs block or rate-limit XML-RPC requests
- Security plugins like Disable XML-RPC are installed on millions of sites
Wait for admin export + import
wp_comments table as ordinary data. The attacker has no control over when the next step fires — it requires a site administrator to perform an export followed by an import using the All-in-One WP Migration plugin. This is the plugin's core function (backup/restore, staging migration, host migration), so it does happen routinely, but timing is unpredictable. During import, the plugin calls Ai1wm_Database::replace_table_values(), which uses a regex to find and rewrite serialized URLs and table prefixes. The regex mishandles backslash-escaped quotes in the attacker's planted data, allowing the payload to escape the string boundary.- Administrator must actively trigger an export+import cycle
- All-in-One WP Migration plugin version ≤ 7.109 is in use
- Timing is entirely admin-dependent — could be hours, weeks, or never
- Sites that only use the plugin for one-time migration (not recurring backups) may never re-trigger
- Automated backup schedules typically use different plugins (UpdraftPlus, BlogVault) that don't trigger this code path
SQL injection extracts ai1wm_secret_key
INSERT or UPDATE statements. The payload is crafted to read the ai1wm_secret_key value from the wp_options table and write it into a new approved comment on the site. This key is a secret token that protects the plugin's unauthenticated import endpoint — leaking it is the critical pivot from data extraction to RCE.- The SQL injection must execute successfully during the import process
- The injected SQL must be able to read wp_options and write to wp_comments
- Database-level permissions are typically sufficient (WordPress DB user has full access to its own schema)
- No additional friction at this step — once step 2 fires, this is automatic
ai1wm_secret_key in comment content.Retrieve secret key via REST API
/wp-json/wp/v2/comments) to retrieve the comment containing the leaked ai1wm_secret_key. Since the injected SQL created a standard approved comment, it is publicly visible through the unauthenticated REST API. This step requires no privileges and can be automated with a simple polling script.- WordPress REST API is publicly accessible (default)
- The comment containing the key has been approved or was inserted as approved
- Some sites disable or restrict the REST API (though this is uncommon for the comments endpoint)
- The attacker must be monitoring the target to catch the key after it appears
Import malicious .wpress archive for RCE
ai1wm_secret_key in hand, the attacker accesses the plugin's unauthenticated import endpoint and uploads a crafted .wpress archive. This archive contains a malicious PHP file placed in the mu-plugins directory (must-use plugins). WordPress automatically loads all PHP files in mu-plugins on every page load — no activation required. The mu-plugin can be a webshell, a reverse shell, or any arbitrary PHP code, giving the attacker full control over the web server. The PoC by 686f6c61 demonstrates this with a benign marker mu-plugin for safe testing.- Valid ai1wm_secret_key obtained from step 4
- The plugin's import endpoint is reachable
- Web server has write permissions to wp-content/mu-plugins (standard WordPress)
- File integrity monitoring (FIM) tools would detect new files in mu-plugins
- Some hosting providers restrict mu-plugins directory writes
- EDR/HIDS on the web server may flag new PHP file creation
wp-content/mu-plugins/. Web server access logs showing POST to the plugin's import endpoint with the secret key parameter. New PHP files appearing outside normal deployment patterns.replace_table_values() so planted payloads cannot escape string literals. Deploy within 30 days per the noisgate remediation SLA for HIGH. For internet-facing production or e-commerce WordPress sites, treat this as urgent and push within 7 days given the public PoC.<Files xmlrpc.php> Require all denied </Files> to your Apache config or equivalent nginx location block, or use the Disable XML-RPC plugin. This should be a standing hardening measure — deploy within 30 days per the noisgate mitigation SLA for HIGH. Breaks the chain at step 1.wp post list --format=ids | xargs wp post update --ping_status=closed via WP-CLI to retroactively close trackbacks on existing posts. This blocks the specific planting vector even if XML-RPC remains open. Deploy within 30 days.wp-content/mu-plugins/ for any new or modified PHP files. The final RCE payload must write a file here. FIM tools (OSSEC, Wazuh, Tripwire, or host-based EDR) will catch the exploitation's final step and enable rapid response. Does not prevent exploitation but ensures detection. Deploy within 30 days.wp_comments for trackback entries containing unusual backslash sequences, hex-encoded strings, or SQL keywords (UNION, SELECT, FROM wp_options). Also search for any comment containing the string ai1wm_secret_key. If found, the planting phase has already occurred — rotate the secret key immediately by deleting and regenerating the ai1wm_secret_key option in wp_options, and do NOT perform any import until patched.- Standard WAF SQL injection rules — second-order SQLi plants data that *looks* like normal text at ingestion time. The malicious SQL only assembles during the plugin's internal find-and-replace, which happens server-side after the WAF has already passed the request. Generic SQLi signatures will miss this at the trackback stage.
- WordPress auto-updates alone — while WordPress supports auto-updates for plugins, many enterprise deployments pin versions or disable auto-updates for change-control reasons. Don't assume your fleet has silently updated.
- Database-level query logging without baseline — the injected SQL executes during a legitimate import operation that already generates thousands of
INSERT/UPDATEstatements. Without a known-good baseline of import queries, anomaly detection on raw query logs will produce excessive false positives.
The supporting signals.
| In-the-Wild Status | No confirmed active exploitation as of 2026-10-08. SOCRadar notes 'exploitation is reported but no independent confirmation is available.' Mass automated exploitation is expected to develop gradually due to the admin-action dependency. |
|---|---|
| Proof-of-Concept | Weaponized public PoC available: 686f6c61/POC-AIOWPM-CVE-2026-19949 — full Docker lab with trackback planting, secret key leak, and RCE via mu-plugin. Also indexed on Vulners and Sploitus. Listed in multiple PoC aggregator repos (asvorg/CVE-2026-PoCs, SecureWithUmer/CVE-2026-PoCs, XZ1r0/cve-2026-poc-collection). |
| EPSS | 0.01429 (≈ top 15th percentile). Moderate probability of exploitation in the next 30 days — reflects the admin-action dependency dampening automated exploitation likelihood. |
| CISA KEV | Not listed as of 2026-10-08. No known addition date. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H — vendor-assigned 8.8 HIGH. noisgate assessment: AC:L and UI:N are generous; the chain requires admin-initiated backup restore (should be UI:R) and is multi-phase/time-delayed (argues for AC:H). Corrected vector would yield ~7.5. |
| Affected Versions | All-in-One WP Migration and Backup ≤ 7.109 (all versions). Plugin by ServMask. 5 million+ active installations per wordpress.org. |
| Fixed Version | 7.110 (released 2026-08-20). Silently labeled as a 'text fix' in the changelog — no security language in the update notice. No distro backports apply (WordPress plugin, not OS-packaged). |
| Exposure Data | WordPress telemetry shows ~35% adoption of 7.110 as of Sep 2, leaving an estimated 3.25 million sites on vulnerable versions. WordPress sites are internet-facing by design; the plugin's import endpoint and XML-RPC are reachable externally by default. |
| Disclosure Timeline | Aug 14: Reported by Jack Taylor to Wordfence Bug Bounty ($5,761 bounty). Aug 15: Disclosed to ServMask. Aug 16: Wordfence premium WAF rule deployed. Aug 20: Patch released (v7.110). Aug 25: CVE published. Sep 2: Full public disclosure. Sep 15: Wordfence free-tier WAF rule. |
| Reporter | Jack Taylor, via the Wordfence Bug Bounty Program. |
Sources.
- BleepingComputer — WordPress backup plugin flaw exposes millions of sites
- CyberSecurityNews — WordPress Plugin Flaw Exposes 5M Sites to SQL Injection
- ToolsLib Blog — CVE-2026-19949 Technical Advisory
- TechTimes — 3.25 Million Sites Exposed, Exploit Code Active
- 686f6c61/POC-AIOWPM-CVE-2026-19949 — Weaponized PoC on GitHub
- DEV Community — From Second-Order SQL Injection on Restore to Site Takeover
- eSecurity Planet — CVE-2026-19949 Leaves Millions Running Vulnerable Versions
- SOCRadar — CVE-2026-19949 CVE Radar
Why this verdict
- Admin-action gate breaks zero-click: The entire chain is inert until a site administrator actively performs an export+import cycle. The attacker cannot trigger, schedule, or accelerate this step. The vendor's
UI:Nis incorrect — this is at minimumUI:R, which alone drops the CVSS from 8.8 to 8.0. - Second-order delay adds attack complexity: The payload must survive dormancy in the database, then execute correctly during a regex-driven find-and-replace pass — a multi-phase, timing-dependent chain. The vendor's
AC:Lunderstates this;AC:His more accurate, yielding a further drop to ~7.5. - Trackback hardening reduces reachable population: While trackbacks are enabled by default, a significant fraction of production WordPress sites disable XML-RPC entirely (recommended by all major WordPress security plugins). This narrows the population that can even receive the initial payload.
- No confirmed in-the-wild exploitation: Despite the public weaponized PoC being available since September 2026, no independent source has confirmed active exploitation. The EPSS of 0.01429 reflects moderate — not urgent — exploitation probability.
- Role multiplier: WordPress is a web-tier application, not a canonical high-value infrastructure component (not an IdP, hypervisor, DC, CA, or PAM). Blast radius on compromise is host-level (webshell on one server) extending to data-breach-level for WooCommerce/e-commerce deployments holding PII or payment data. However, it does not reach domain/fleet/supply-chain scope. The ~3.25M exposed sites are internet-facing by design, which keeps severity meaningful — a compromised WordPress server is a foothold for lateral movement and a platform for malware distribution. This supports HIGH but does not warrant CRITICAL. No high-value-role floor override applies.
Why not higher?
A CRITICAL rating would require either a zero-click/wormable chain or a canonical high-value infrastructure target. This CVE has neither: the chain depends on an unpredictable admin action (export+import), and WordPress — while massively deployed — is a web application tier, not an identity system, hypervisor, or domain controller. The host-level blast radius, while serious for individual sites, does not produce fleet-scale or domain-scale impact. The EPSS of 0.01429 and absence from CISA KEV further argue against CRITICAL.
Why not lower?
Despite the friction, the fundamentals are dangerous: the initial vector is unauthenticated and remote, the end result is full RCE, the attack surface is 3.25 million internet-facing sites, and a weaponized public PoC exists with Docker reproduction steps. Wordfence correctly notes that backup/restore is the *core purpose* of this plugin — admins WILL trigger it eventually. The payload is patient; it doesn't expire. A MEDIUM rating would understate the risk for any organization running this plugin on a production site.
Crowdsourced verification payload.
Run this script on each WordPress host (or from a management/jump box with filesystem access to WordPress installations). It checks the installed version of All-in-One WP Migration against the patched version 7.110. Requires read access to the WordPress plugin directory. Example: sudo bash check_cve_2026_19949.sh /var/www/html
#!/usr/bin/env bash
# check_cve_2026_19949.sh — Detect CVE-2026-19949 in All-in-One WP Migration
# Usage: bash check_cve_2026_19949.sh /path/to/wordpress
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
set -euo pipefail
WP_ROOT="${1:-/var/www/html}"
PLUGIN_FILE="${WP_ROOT}/wp-content/plugins/all-in-one-wp-migration/all-in-one-wp-migration.php"
SAFE_VERSION="7.110"
if [[ ! -f "$PLUGIN_FILE" ]]; then
echo "UNKNOWN — All-in-One WP Migration plugin not found at ${PLUGIN_FILE}"
# Also check mu-plugins for IOCs while we're here
MU_DIR="${WP_ROOT}/wp-content/mu-plugins"
if [[ -d "$MU_DIR" ]] && find "$MU_DIR" -name '*.php' -newer "$WP_ROOT/wp-config.php" -print -quit 2>/dev/null | grep -q .; then
echo "WARNING — Suspicious recent PHP files found in mu-plugins directory"
fi
exit 2
fi
# Extract version from plugin header
INSTALLED_VERSION=$(grep -i '^[[:space:]]*\*[[:space:]]*Version:' "$PLUGIN_FILE" | head -1 | sed 's/.*Version:[[:space:]]*//' | tr -d '[:space:]')
if [[ -z "$INSTALLED_VERSION" ]]; then
echo "UNKNOWN — Could not parse version from ${PLUGIN_FILE}"
exit 2
fi
# Version comparison function
version_gte() {
printf '%s\n%s' "$1" "$2" | sort -V | head -1 | grep -qx "$2"
}
echo "Installed version: ${INSTALLED_VERSION}"
echo "Safe version: >= ${SAFE_VERSION}"
if version_gte "$INSTALLED_VERSION" "$SAFE_VERSION"; then
echo "PATCHED — All-in-One WP Migration ${INSTALLED_VERSION} is >= ${SAFE_VERSION}"
exit 0
else
echo "VULNERABLE — All-in-One WP Migration ${INSTALLED_VERSION} is affected by CVE-2026-19949"
# Check for IOCs: trackbacks with suspicious content
WP_CONFIG="${WP_ROOT}/wp-config.php"
if [[ -f "$WP_CONFIG" ]]; then
DB_NAME=$(grep "DB_NAME" "$WP_CONFIG" | cut -d "'" -f4 2>/dev/null || true)
DB_PREFIX=$(grep "table_prefix" "$WP_CONFIG" | cut -d "'" -f2 2>/dev/null || true)
if [[ -n "$DB_NAME" && -n "$DB_PREFIX" ]]; then
echo "TIP: Check for planted payloads with:"
echo " mysql -e \"SELECT comment_ID, comment_content FROM ${DB_NAME}.${DB_PREFIX}comments WHERE comment_type='trackback' AND (comment_content LIKE '%ai1wm_secret_key%' OR comment_content LIKE '%UNION%SELECT%' OR comment_author LIKE '%\\\\\\\\%');\" "
fi
fi
# Check for post-exploitation IOCs in mu-plugins
MU_DIR="${WP_ROOT}/wp-content/mu-plugins"
if [[ -d "$MU_DIR" ]]; then
SUSPICIOUS=$(find "$MU_DIR" -name '*.php' -newer "$PLUGIN_FILE" 2>/dev/null | head -5)
if [[ -n "$SUSPICIOUS" ]]; then
echo "WARNING — Suspicious PHP files in mu-plugins (possible post-exploitation):"
echo "$SUSPICIOUS"
fi
fi
exit 1
fi