The exploit sits in your comments table doing nothing. It fires the day you restore a backup, and updating the plugin does not rotate the key it steals.
CVE-2026-19949 is an 8.8 in All-in-One WP Migration and Backup, a plugin on five million WordPress sites. An attacker plants two trackbacks on a public post, each ending in a backslash, and then waits. Nothing happens until an administrator restores an archive, at which point the plugin's URL-rewriting code mis-parses where a SQL string ends, promotes the planted text to executable SQL, writes the plugin's own secret key into a public comment, and hands an unauthenticated attacker remote code execution via a crafted .wpress archive. We diffed 7.109 against 7.110: the entire fix is one regular expression. We also read the key-generation code, and it matters, because the key is twelve characters, is created once, and is never rotated by an update. We pulled WordPress.org's version API this morning: 39.19 percent of installs have taken the patch, which leaves about 3.04 million that have not. Here is the chain, the actual diff, why patching is only half the job, and what the headlines calling this actively exploited are getting wrong.
Most vulnerabilities have a clean shape. An attacker sends a request, something bad happens, and the gap between those two events is measured in milliseconds. You can reason about that. Patch the thing, and the request stops working.
CVE-2026-19949 does not have that shape. An attacker sends two entirely ordinary requests to your site, both of which succeed, both of which look like nothing, and then nothing happens at all. Not that day, not that month. The bad thing happens later, on a day you choose, triggered by you, doing the single most responsible thing a site owner can do: restoring a backup.
That gap is the entire story, and it is why the standard advice is incomplete here. Updating the plugin closes the door. It does not do anything about what may already have walked through it, and in this particular case what walks through it is a twelve-character string that an update will not change.
The facts, first
The plugin is All-in-One WP Migration and Backup, published by ServMask. WordPress.org’s plugin API reports 5,000,000 active installations and 187 million lifetime downloads. It has been on the repository since January 2014. If you have ever moved a WordPress site between hosts, there is a good chance you used this, and a better chance it is still installed.
| Field | Value |
|---|---|
| CVE | CVE-2026-19949 |
| Type | Second-order SQL injection (CWE-89) |
| CVSS 3.1 | 8.8 High, AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Affected | All versions up to and including 7.109 |
| Patched | 7.110, released August 20, 2026 |
| Reported | August 14, 2026, by Jack Taylor via the Wordfence Bug Bounty Program |
| Bounty | $5,761 |
| CVE published | August 25, 2026 (NVD), broad coverage from September 2 |
Wordfence shipped a firewall rule to Premium, Care and Response customers on August 16. Free-tier Wordfence users are scheduled to receive the same rule on September 15. That is worth holding onto, because it means that as of today a large number of sites running Wordfence believe they are covered and are not. We will come back to it.
The chain, step by step
This is the part the summaries compress into a sentence, and compressing it is what makes it sound abstract. It is not abstract. It is seven concrete steps.
One. The attacker submits two trackbacks to any public post on your site. Trackbacks are a WordPress feature from 2003: a way for one blog to notify another that it linked to it. Each trackback carries a URL. In this attack each URL ends in a trailing backslash, and points at a payload.
Two. WordPress stores them. It does not strip the backslashes and it has no reason to reject the URLs. They are now two rows in your wp_comments table. They may be pending moderation. They may be visible. Either way they are stored, and storage is all the attacker needed.
Three. Nothing happens. This is the step people skip. The rows sit there. Your site behaves normally. Nothing in your access log looks unusual, because nothing unusual occurred: somebody sent a trackback, which happens to blogs constantly.
Four. Weeks or months later, you migrate. You export the site with All-in-One WP Migration, and you import that archive somewhere: a new host, a staging environment, a rebuild after an incident, a routine restore drill. This is the plugin’s entire reason to exist. Wordfence’s own framing calls it “a routine action.”
Five. During the restore, the plugin walks the SQL dump inside the .wpress archive and rewrites the old site’s URL to the new one. To do that it has to find the string literals inside each SQL statement, because it should only rewrite text inside values, never SQL syntax. It finds them with a regular expression, and the regular expression is wrong. The attacker’s trailing backslashes make it mis-identify where a string ends. Content the attacker planted stops being a value and becomes SQL. Then it runs.
Six. The injected SQL reads the option ai1wm_secret_key out of wp_options and writes it into a comment. That comment is approved, so it is public. The attacker retrieves it from your site’s ordinary comments REST API endpoint, /wp-json/wp/v2/comments. No credentials. No unusual endpoint. A public read of a public API.
Seven. That secret key is the only thing standing between the internet and the plugin’s import function. With it, the attacker imports a crafted .wpress archive containing a malicious must-use plugin, which WordPress executes on the next page load. That is remote code execution as the web server user, and from there the site is not yours.
The bug is one regular expression
We pulled both versions of the file from the WordPress.org plugin SVN and diffed them. The advisory points at lib/vendor/servmask/database/class-ai1wm-database.php. Here is the whole security-relevant change between 7.109 and 7.110:
// Replace serialized values
foreach ( $this->get_old_replace_values() as $old_value ) {
if ( strpos( $input, $this->escape( $old_value ) ) !== false ) {
- $input = preg_replace_callback( "/'(.*?)(?<!\\\\)'/S", ... );
+ $input = preg_replace_callback( "/'((?:[^'\\\\]++|\\\\.)*+)'/sS", ... );
break;
}
}
One line. That is the fix.
Read the old pattern as a regular expression rather than as a PHP string and it is /'(.*?)(?<!\\)'/S. In English: find a quote, take as little as possible, stop at the next quote that is not preceded by a backslash.
The intent is obvious and the reasoning is the mistake almost everyone makes the first time they parse escaped strings by hand. A backslash before a quote usually means the quote is escaped and therefore is not the end of the string. Usually. But a backslash can also itself be escaped. In a MySQL dump, \\ is one literal backslash, and a quote immediately after it is a real closing quote.
The lookbehind cannot tell those two cases apart. It sees one character. So when a value legitimately ends in a backslash, the pattern refuses the real closing quote and runs on to the next quote it finds, which is the opening quote of the following value. Every quote boundary after that point is off by one. What was inside a string is now outside it, and what was SQL punctuation is now treated as string content. The callback then unescapes the span it captured, which converts the attacker’s escaped quotes into real ones, re-escapes, and emits the result back into a statement that is subsequently executed.
That is why the attack needs two trackbacks. One to knock the boundary out of alignment, one to close the misaligned span so that the text between them lands in statement position.
The replacement pattern, /'((?:[^'\\]++|\\.)*+)'/sS, does the thing you are supposed to do: consume either a character that is neither a quote nor a backslash, or a backslash together with whatever follows it, treating the pair as one indivisible unit. An escaped backslash is now consumed as a backslash, so the quote after it is correctly recognised as the end of the string. The possessive quantifiers (++, *+) are there to prevent catastrophic backtracking, which is a second bug class you get for free when you write this correctly.
We are showing you the diff for a reason that has nothing to do with WordPress. Any code that unescapes attacker-influenced text and then re-emits it into a structured language has this bug available to it, and the failure is almost never in the escaping. It is in the boundary detection that runs before the escaping. If you have hand-rolled string parsing anywhere in your stack, this is the shape of the thing to go and look for.
The part that survives the update
Now the detail that is not in any of the coverage, and that changes what you should actually do.
We read the plugin’s key handling in the current 7.110 source. The secret key is created here:
public function setup_secret_key() {
if ( ! get_option( AI1WM_SECRET_KEY ) ) {
update_option( AI1WM_SECRET_KEY, ai1wm_generate_random_string( 12 ) );
}
}
And it is checked here, in ai1wm_verify_secret_key(), which is the sole authorisation on the import path:
if ( $secret_key !== get_option( AI1WM_SECRET_KEY ) ) {
throw new Ai1wm_Not_Valid_Secret_Key_Exception( ... );
}
Three things follow, and all three matter.
It is twelve characters from a 62-character alphabet. That is fine as a secret, and it is not the problem.
It is created only if the option is absent. There is no rotation, no expiry, and no regeneration on update. The key your site generated the first time you loaded the plugin’s admin page is the key it is using today.
It is a standalone authenticator. The import controller checks the key and nothing else. There is even a filter in the plugin that lets a valid secret key authenticate REST routes after an import has wiped the site’s user credentials, which is a sensible thing to build and a very unfortunate thing to have leaked.
Put those together. If your site restored an archive while running 7.109 or earlier, and somebody had planted trackbacks beforehand, then your secret key left the building. Installing 7.110 fixes the parser so it cannot happen again. It does not change the key. The attacker still has a valid credential for the import endpoint on a fully patched site.
So the remediation has two halves, and every piece of coverage we read only published the first one.
The numbers, pulled this morning
WordPress.org publishes a version-distribution API for every plugin. We queried it for all-in-one-wp-migration on the morning of September 5, 2026:
| Version | Share of installs |
|---|---|
| 7.110 (patched) | 39.19% |
| 7.105 | 6.17% |
| Everything else | 54.61% |
Against five million active installations, that is roughly 1.96 million patched and 3.04 million not.
It is moving, which is the good news. BleepingComputer and SecurityWeek both reported approximately 35 percent patched on September 2 and 3, leaving about 3.25 million exposed. Three days later it is 39.19 percent. Roughly 200,000 sites took the update over a long weekend, which is a faster curve than we normally see and is almost certainly the coverage doing its job.
The bad news is the shape of the remainder. We have now measured this same curve on three separate WordPress stories this year, most recently in our wp2shell post, and it always looks the same: a fast initial climb driven by sites with automatic plugin updates enabled, then a long flat tail of sites that nobody is maintaining and that will still be unpatched next year. The 54.61 percent in the “everything else” bucket includes releases going back years.
Update, September 6, 2026: we re-queried the same API twenty-four hours later. 7.110 is now at 39.83 percent, 7.105 at 6.08 percent, and everything else at 54.06 percent. That is 0.64 points in a day, which against five million installs is roughly 32,000 sites patched since yesterday, and it puts the totals at about 1.99 million patched and 3.01 million not. The curve is already flattening: it moved four points over the long weekend and two thirds of a point yesterday. If it keeps decaying at that rate, the population still exposed at the end of September is close to three million, which is the flat tail arriving on schedule rather than anything new going wrong.
We also re-pulled the KEV feed on the morning of September 6. Catalog version is still 2026.09.04 with 1,695 entries, unchanged since Thursday, and CVE-2026-19949 is still not on it. The assessment below stands.
What the headlines are getting wrong
Several outlets are running this as actively exploited with weaponised exploit code in circulation. We went and checked the primary sources, because that claim changes the urgency and it is the kind of claim that gets repeated without anyone verifying it.
CISA’s own assessment says otherwise. NVD’s record for CVE-2026-19949 carries an SSVC decision point block, filed by the CISA Coordinator on August 27, 2026. It reads: exploitation: none, automatable: no, technicalImpact: total. CISA looked at this and concluded there was no evidence of exploitation.
It is not in the Known Exploited Vulnerabilities catalog. We pulled the KEV feed today. Catalog version 2026.09.04, 1,695 entries. CVE-2026-19949 is not one of them. The most recent additions are the September 2 batch (LiteLLM, Starlette, Kestra, JFrog Artifactory, Sangoma, two SonicWall SMA1000 flaws) and Chrome’s V8 bug on September 4.
The two most careful outlets do not make the claim. BleepingComputer’s piece contains no assertion of exploitation or public exploit code. eSecurity Planet’s does not either. SecurityWeek describes the mechanism in detail and reports no in-the-wild activity.
There is also a smaller discrepancy worth naming, because someone will notice it and wonder. The published CVSS vector includes PR:L, meaning privileges required: low, while the description and every write-up describe an unauthenticated attacker. Both are defensible: submitting a trackback needs no account at all, and the injected SQL runs during an administrator-initiated restore. The vector is scoring a different moment in the chain than the prose is. It does not change the answer.
None of this makes the bug less serious. technicalImpact: total is CISA agreeing that a successful attack ends with full control. What it changes is the character of the risk. This is not a mass-scanning event where bots are compromising sites right now. It is a bug that requires patience and a specific trigger, which means the population at risk is not “everyone running the plugin”, it is everyone running the plugin who is going to restore an archive. That is a smaller group, and it is exactly the group that will not find out until afterwards. This is precisely the reasoning CISA formalised in BOD 26-04, which we walked through in August: the question is never only how bad, it is how bad multiplied by how reachable.
What to do, in order
1. Check your version. WordPress admin, Plugins, find All-in-One WP Migration and Backup. 7.110 or later is patched. With WP-CLI:
wp plugin list --name=all-in-one-wp-migration --fields=name,version,status
If it says 7.109 or lower, update now, before you read the rest of this.
2. If you are not using it, delete it. This is a migration tool. Most sites install it once, move, and never touch it again. An installed and deactivated plugin still ships its files to your server, and this specific plugin also leaves its secret key sitting in your options table. If you moved hosts in 2023 and it has been idle since, remove it. That is a complete fix and it takes ten seconds.
3. Answer the only question that matters: have you restored an archive? Not exported. Imported or restored. If you have never run a restore with this plugin, the injected SQL never executed, your key never leaked, and steps 4 and 5 are precautionary rather than urgent. If you have restored at any point while running 7.109 or earlier, treat the key as compromised and keep going.
4. Rotate the secret key. Nobody else is publishing this step and it is the one that closes the actual exposure. The plugin regenerates the key when the option is missing, on the next admin_init:
wp option get ai1wm_secret_key # note the current value first
wp option delete ai1wm_secret_key
# now load any wp-admin page, then confirm it changed:
wp option get ai1wm_secret_key
Without WP-CLI, delete the ai1wm_secret_key row from wp_options in phpMyAdmin and then load your dashboard.
5. Check whether the key is already sitting in a public comment. Before you rotate, take the old value and look for it. Via the public API, which is exactly how an attacker would look:
curl -s "https://yoursite.com/wp-json/wp/v2/comments?per_page=100&search=OLDKEYVALUE"
Or directly in the database:
SELECT comment_ID, comment_post_ID, comment_date, comment_approved
FROM wp_comments
WHERE comment_content LIKE '%OLDKEYVALUE%';
Anything returned is confirmation, not suspicion. Rotate the key, then go to step 7.
6. Look at your trackbacks. Most sites have very few, which makes this a short list to eyeball:
wp comment list --type=trackback --fields=comment_ID,comment_date,comment_author_url,comment_approved
You are looking for author URLs ending in a backslash. In SQL, across both trackbacks and pingbacks:
SELECT comment_ID, comment_date, comment_type, comment_author_url
FROM wp_comments
WHERE comment_type IN ('trackback','pingback')
ORDER BY comment_date DESC;
A planted trackback is not proof of compromise on its own. It is proof that somebody set the trap, which tells you whether to take step 3 seriously.
7. If the key leaked, check for the payload. The documented end state is a must-use plugin, which WordPress loads automatically on every request and which does not appear in the normal plugins list. Look at the directory directly:
ls -la wp-content/mu-plugins/
Most sites have nothing there, or one file put there deliberately by their host or their developer. Anything you cannot account for is the finding. While you are there, list your administrators and confirm you recognise every one, and rotate the authentication keys and salts in wp-config.php, which invalidates every existing session. The persistence checks are identical to the ones in our wp2shell post, because the logic is identical: patching closes the door, it does not remove anyone already inside.
8. Turn trackbacks off. They are a 2003 feature, they are overwhelmingly used for spam in 2026, and on most business sites they provide nothing. New posts:
wp option update default_ping_status closed
Existing posts, which the setting above does not touch:
wp post list --post_type=any --ping_status=open --format=ids | xargs -r wp post update --ping_status=closed
Through the admin instead: Settings, Discussion, uncheck “Allow link notifications from other blogs (pingbacks and trackbacks) on new articles.” Then Posts, select all, Bulk actions, Edit, and set Pings to Do not allow.
9. If you rely on Wordfence, check which tier you are on. The rule reached Premium, Care and Response on August 16. Free users get it on September 15. If you are on the free tier, you have ten days with no firewall coverage for this, and the update in step 1 is the only thing protecting you.
The uncomfortable pattern
This is the third WordPress story we have written in six weeks, and they rhyme in a way that is worth saying out loud.
TranslatePress leaked an admin password reset link because a translation layer captured and stored a string it should never have seen. wp2shell chained two core bugs into pre-auth RCE. This one turns a backup tool into a delivery mechanism. Different vendors, different subsystems, no shared code. The common factor is reach: each of these components sits in the path of everything, which means its bug surface is the size of your entire site.
Backup and migration tooling is the most extreme case of that, and it is the one people think about least, because it registers as a safety feature rather than as software. It reads every table. It writes every table. It runs with an authenticator that bypasses your login. It is, functionally, a second admin account that you never audit and that has no password policy. When the code that restores your site is the code that compromises it, the ordinary instinct (restore from backup and move on) is the thing that hurts you.
None of that is an argument against backups. It is an argument for knowing which of your plugins have that reach, keeping those specific ones current on a days timeline rather than a monthly one, and removing the ones you finished using three years ago. If you cannot currently name the four or five plugins on your site with that kind of access, that is the finding, and it is the same finding we arrived at from a completely different direction in Astro vs Next.js vs WordPress: the cost of a WordPress site is not the build, it is knowing what is running on it.
If you want a second opinion on a site you own, send us the URL. We will tell you what is running, which plugins have the reach to matter, whether any of them are currently unpatched, and whether this particular plugin is sitting on your server with a key it generated years ago. It takes about ten minutes and it does not cost anything.
Sources
- CVE-2026-19949, National Vulnerability Database, published August 25, 2026, last modified August 27, 2026, retrieved September 5, 2026, for the CVSS vector, affected versions, CWE and the CISA Coordinator SSVC decision points
- All-in-One WP Migration and Backup, source at tags 7.109 and 7.110, WordPress.org plugin SVN, retrieved September 5, 2026, for the
class-ai1wm-database.phpdiff,setup_secret_key(),ai1wm_generate_random_string()andai1wm_verify_secret_key() - WordPress.org plugin information API, retrieved September 5, 2026, for active installs, current version and release date
- WordPress.org plugin version statistics API, retrieved September 5 and again September 6, 2026, for the version-share table and the twenty-four hour movement
- CISA Known Exploited Vulnerabilities catalog, catalog version 2026.09.04, retrieved September 5 and again September 6, 2026, confirming CVE-2026-19949 is not listed
- WordPress backup plugin flaw exposes millions of sites to takeover attacks, BleepingComputer, September 3, 2026, for the disclosure timeline, bounty and patch adoption figure
- Over 3 Million WordPress Sites Affected by Migration Plugin Vulnerability, SecurityWeek, September 2026, for the two-trackback mechanism, the comments REST API retrieval step and the malicious must-use plugin end state
- CVE-2026-19949 Leaves Millions of WordPress Sites Running Vulnerable Plugin Versions, eSecurity Planet, September 2026
- CVE-2026-19949: Unauthenticated SQL Injection in All-in-One WP Migration, ToolsLib, September 1, 2026, for the Wordfence firewall rule dates