For nearly two decades, administrators have treated the update button as the end of a vulnerability. You fetch an official archive over TLS from a trusted upstream, verify its checksums, apply it, and the asset moves from vulnerable to hardened. CVE-2026-119977 breaks that assumption. It is an unauthenticated remote code execution (RCE) race condition affecting WordPress 2.7 through 7.1.3, credited to Umut Bayram of Picus Security and independently discovered and analyzed by researchers at Xint. The flaw turns the core update mechanism itself into a short-lived, unauthenticated execution path.
What bothers me most is the transition exposure. An unpatched WordPress installation has to use its own vulnerable updater code to unpack the security patch meant to fix it, so installing WordPress 7.1.4 exposes certain sites to pre-authentication compromise one final time.
The vulnerability carries a CVSS v3.1 score of 8.1 and joined the U.S. Cybersecurity and Infrastructure Security Agency (CISA) Known Exploited Vulnerabilities (KEV) catalog on November 3, 2026. The newly released code is not where the flaw lives. CVE-2026-119977 is a classic Time-of-Check to Time-of-Use (TOCTOU) failure in how WordPress stages, validates, and copies files across a web-accessible filesystem during a routine core upgrade. To see why it went unnoticed since WordPress 2.7 (released in 2008), look at what the server becomes while an upgrade is in flight: a dual-instance PHP environment where a pristine, unconfigured copy of WordPress sits in a predictable subdirectory of the live web root, waiting for anyone on the internet to configure it.
CVE-2026-119977 at a glance
- Vulnerability type: unauthenticated RCE through a TOCTOU race condition in the WordPress core updater
- Affected versions: WordPress 2.7 through 7.1.3
- CVSS v3.1 score: 8.1
- Credit: Umut Bayram (Picus Security), with independent analysis from Xint
- CISA KEV listing: November 3, 2026, with a federal civilian remediation deadline of November 6
- Fixed in: WordPress 7.1.4, released October 28, 2026, plus 24 backported releases back to the 4.7 branch
- Exploitation status: active, with automated attempts starting within minutes of the patch release
How a WordPress core upgrade works step by step
When an update is triggered, WordPress does not overwrite core files in place from memory. The trigger can be an administrator clicking “Update Now” under Dashboard > Updates, WP-CLI (wp core update), or the wp-cron background updater. In every case the work runs through the WP_Upgrader and Core_Upgrader classes, historically housed in wp-admin/includes/class-wp-upgrader.php (and in wp-admin/includes/update.php for legacy 2.7.x releases). Together they execute a six-stage filesystem transaction:
- The local installation queries the WordPress.org update API to learn which archive format fits its current version and file integrity state.
- The updater downloads the selected official ZIP archive to a temporary location.
- The currently installed updater code creates a working staging directory inside
wp-content/upgrade/and extracts the ZIP contents into it. - The existing installation validates the extracted package and loads the newly unpacked
wp-admin/includes/update-core.phpinto memory, which hands the remaining steps to the new release’s deployment logic. - The
update_core()function enumerates the files in thewp-content/upgrade/<working_dir>/staging directory, copies them over the live root directory, and runs any required database schema migrations. - The updater deletes the temporary staging directory and removes the
.maintenancelock file from the site root.
The vulnerability comes from three behaviors in steps three and five.

Why the wp-content/upgrade directory is exposed
In every standard Apache, Nginx, or LiteSpeed deployment using the default WP_CONTENT_DIR configuration, wp-content/ sits directly inside the public document root so browsers can fetch theme stylesheets, JavaScript assets, and media uploads. WordPress never shipped an .htaccess rule, Nginx snippet, or execution lock that stops the PHP interpreter from running scripts inside wp-content/upgrade/. Any .php file unpacked there during step three therefore becomes a live, externally routable HTTP endpoint for as long as the upgrade runs.
That endpoint exists for a few seconds, or tens of seconds on slow shared hosting with high disk I/O latency. An attacker needs the exact URL of the staging folder before extraction begins, and in WordPress 5.9 through 7.1.3 (as well as 2.7 through 4.3) WP_Upgrader::unpack_package() made that easy. It built the temporary directory name deterministically by stripping the .zip extension from the downloaded package basename. If a site downloaded wordpress-7.1.3-no-content.zip, the updater created wp-content/upgrade/wordpress-7.1.3-no-content/ and unpacked the distribution into a nested wordpress/ folder.
Between WordPress 4.4 and 5.8, core behavior incidentally appended random characters or suffixes to the directory name under certain collision or path-handling conditions. That masked the name unless the server leaked paths through PHP warnings or open directory indexing. Xint’s full-tag audit still confirmed that the underlying weakness spans the entire 2.7 to 7.1.3 lineage.
The third component sits in step five. When update-core.php prepared to move staged files into the live root, it did not iterate over an immutable manifest built from the original ZIP central directory. It scanned whatever was on disk in the temporary directory right before copying. Any file injected or generated in that folder between extraction (step three) and copying (step five) was treated as an authentic part of the official release and copied into the production web root.
Which WordPress update packages are exploitable
Whether a staging folder contains an exploitable entry point depends on which ZIP variant the WordPress.org API tells the local updater to download. The release pipeline distributes four package types for core updates:
📬 Stay Ahead of Cyber Threats
Get the latest cybersecurity news, critical vulnerabilities, threat intelligence, tutorials, and exclusive giveaways delivered straight to your inbox. No spam. Unsubscribe anytime.
Subscribe to the Newsletter →- partial: only the files modified between the site’s exact starting version and the target version
- no-content: the complete core codebase without the
wp-content/directory, so no default themes such as Twenty Twenty-Five and no bundled plugins such as Akismet - new-bundled: the complete core codebase plus any default themes or plugins introduced when crossing major version boundaries
- full: the complete standalone distribution, identical to the manual download from WordPress.org
The local installation evaluates the candidate packages in a strict hierarchy. It picks the partial package only if three conditions hold: a partial package exists for the exact installed version string, the operation is a standard upgrade and not a manual core reinstall, and a pre-update checksum check confirms the local core files match the official WordPress.org hashes. If any check fails, the updater falls back to new-bundled, then no-content, and finally full. A failure can happen when an administrator modified a core file, when no direct partial exists because the site is jumping across older branches, or when someone clicks “Re-install version.”
This hierarchy decides exploitability. The full, no-content, and new-bundled archives always include the complete installer suite, specifically wp-admin/setup-config.php. A partial archive includes setup-config.php only if that file changed between the source and destination releases. Without it and its include dependencies, the extracted files cannot run as a standalone installation wizard, and the attack chain never starts.
How the exploit works: from setup-config.php to arbitrary PHP execution
The attacker takes advantage of how WordPress decides whether it is already installed. When setup-config.php is requested over HTTP, it looks for wp-config.php in its relative root directory (../wp-config.php) and one level above that (../../wp-config.php). The staged copy lives deep inside wp-content/upgrade/<package-name>/wordpress/wp-admin/setup-config.php, so those lookups check wp-content/upgrade/<package-name>/wordpress/ and wp-content/upgrade/<package-name>/. Neither holds the live site’s wp-config.php, which sits three levels higher in the primary document root. The script concludes it is running inside a brand-new, unconfigured WordPress deployment and accepts unauthenticated POST requests (?step=2) carrying external MySQL database connection parameters.
The attacker needs no credentials, no SSH access, and no man-in-the-middle position. They need an externally reachable MySQL server holding a WordPress database schema with a pre-configured administrator account. They calculate the predictable staging URL and keep sending POST requests that supply their remote database IP, username, and password. While the site runs normally, those requests hit a non-existent path and return 404 Not Found. Once an administrator or cron job triggers an update and extraction finishes, one of the in-flight POST requests lands on the freshly unpacked setup-config.php.
What happens next depends on whether the target is updating to a pre-7.1.4 release or moving to the patched 7.1.4 release itself.
updating to an unpatched release (live configuration overwrite)
Xint researchers validated the full chain by updating a WordPress 7.0.4 instance to the unpatched 7.1.3 no-content ZIP:
- During step three, the 7.0.4 updater extracted the archive into
wp-content/upgrade/wordpress-7.1.3-no-content/wordpress/. - The attacker’s continuous POST loop hit
setup-config.php?step=2inside that folder. The victim’s PHP worker opened an outbound MySQL connection to the attacker’s database server, completed the handshake, and wrote a newwp-config.phpinto the staging folder withDB_HOSTpointing at the attacker’s infrastructure. - In step four, the updater loaded the unpatched 7.1.3
update-core.php. - In step five,
update-core.phpscanned the staging directory, found the newwp-config.phpnext to the legitimate core files, and copied the whole tree into/var/www/html/, overwriting the productionwp-config.php.
After the updater cleaned up the temporary folder, the live site was permanently wired to the attacker’s MySQL database. The attacker then opened the live /wp-admin/ login page, signed in with the administrator credentials stored in their own database, uploaded a malicious plugin ZIP containing a PHP web shell through the standard plugin uploader, and had persistent RCE as the www-data user.
What the WordPress 7.1.4 patch changes
On October 28, 2026, WordPress released version 7.1.4 alongside 24 backported security releases across every maintenance branch back to 4.7: 7.0.8, 6.9.11, 6.8.12, 6.7.11, 6.6.11, 6.5.14, 6.4.14, 6.3.14, 6.2.15, 6.1.16, 6.0.18, 5.9.20, 5.8.19, 5.7.21, 5.6.23, 5.5.24, 5.4.25, 5.3.27, 5.2.30, 5.1.28, 5.0.31, 4.9.35, 4.8.34, and 4.7.39. The official fix adds three hardening measures:
WP_Upgrader::unpack_package()now appends a 16-character cryptographically random string to the temporary working directory name insidewp-content/upgrade/.- Before extracting files, the upgrader creates a blank
wp-content/upgrade/index.php, so servers withOptions +Indexesor autoindex enabled cannot leak the randomized directory name through a directory listing. - The updated
wp-admin/includes/update-core.phpexplicitly excludeswp-config.phpwhen copying from the staging directory to the live root, so a staged configuration file can no longer overwrite the live one.
Why updating to WordPress 7.1.4 can still be exploited
Umut Bayram and Xint both flagged a catch. The code that performs step three (directory creation and ZIP extraction) is the old class-wp-upgrader.php already on disk. The code that performs step five (copying files into the live site) is the new update-core.php inside the downloaded ZIP.
Take an administrator on an unpatched release such as WordPress 7.0.7 or 6.9.10 who clicks “Update Now” to reach 7.1.4. WordPress.org offers no partial package for cross-branch jumps, so the site receives a complete (no-content or full) package that contains setup-config.php. The updater still runs 7.0.7’s class-wp-upgrader.php at the start, so it creates a predictable directory name one last time: wp-content/upgrade/wordpress-7.1.4-no-content/.
If an attacker hits setup-config.php during that extraction window, wp-config.php gets written into the staging folder. Step four then loads the new 7.1.4 update-core.php, which refuses to copy that malicious wp-config.php over the live configuration, so the live database connection stays safe. Until step six deletes the staging folder, though, the directory wp-content/upgrade/wordpress-7.1.4-no-content/wordpress/ is a fully working WordPress installation connected to the attacker’s database.
This ephemeral second instance runs under the same OS user as production (www-data, nginx, or the cPanel account user), so the attacker does not have to wait for the update to finish. While update-core.php is busy copying thousands of core files, an automated script can authenticate to the temporary instance under wp-content/upgrade/.../wp-admin/ with a pre-seeded session cookie from the attacker’s database. From there it can use plugin installation or theme file editing inside the staging instance to execute arbitrary PHP and write a persistent backdoor into the live /var/www/html/ web root before step six removes the staging directory.
Which WordPress sites are still at risk
Xint’s analysis explains why every WordPress instance was not exploitedd on October 28. Unmodified installations of WordPress 7.1, 7.1.1, 7.1.2, or 7.1.3 that updated to 7.1.4, manually or through background auto-updates, received a partial archive. wp-admin/setup-config.php did not change between 7.1 and 7.1.4, so those four official partial ZIPs did not contain it. Their unpatched updaters unpacked the archive into a predictable directory, but the directory lacked the installer script the attack needs.
Sites on older branches (4.7 through 7.0) that allowed automatic background security updates within their minor branch, such as 7.0.7 updating to 7.0.8, also received branch-specific partial ZIPs without setup-config.php. Once 7.0.8 or 7.1.4 finished installing, the new class-wp-upgrader.php was on disk, so any later major upgrade (7.0.8 to 7.1.4, or to 7.2 through a full package) extracts into a randomized 16-character directory.
Two large groups remain exposed during their next update:
- Any unpatched site from 2.7 through 7.0.7 that performs a manual update to 7.1.4, and any 7.1.x site with modified core files, which fails checksum verification and falls back to the no-content or full ZIP.
- Any site still on an unpatched 7.1 to 7.1.3 release that delays patching until a later version such as 7.1.5 or 7.2. If a direct partial is no longer offered by then, or
setup-config.phpchanges in that release, the site’s unpatched 7.1.3 updater will unpack a full installer into a predictable folder.
CVE-2026-119977 exploitation in the wild
Weaponization moved fast. WordPress.org published the 7.1.4 security advisory on October 28, 2026, at 17:13:02 UTC, while Xint’s independent October 25 report was still moving through third-party triage. Wordfence, which assigned the CVE-2026-119977 identifier, reported that automated exploitation attempts began nine minutes after it deployed its initial Web Application Firewall (WAF) rule on release day. Its sensors recorded more than 18,500 malicious requests against staging installer paths in the first 21 hours, and over 63,000 exploitation attempts across the internet within 24 hours of the patch.
The traffic analysis shows disciplined behavior built for a narrow race window. Two of the most active attacking IP clusters skipped noisy horizontal scans across millions of hosts. They focused on small groups of WordPress installations and polled them relentlessly for about 14.5 hours straight. Because the staging folder exists for only a few seconds during an upgrade, periodic reconnaissance cannot catch it. Attackers have to keep a high-frequency request loop running against target domains until an administrator in that time zone reads the advisory and clicks “Update Now.” Public proof-of-concept repositories appeared on GitHub earlier this week, and CISA set a November 6 remediation deadline for federal civilian executive branch agencies, so opportunistic actors have now automated the race loop.
How to fix CVE-2026-119977 safely before updating WordPress
Running wp core update through WP-CLI and clicking “Update Now” in the dashboard invoke the same vulnerable WP_Upgrader code path. Administrators of unpatched instances (2.7 through 7.1.3) therefore need to close the staging exposure before they start the update. Three engineering paths work without exposing the server during the transition.
Option 1: block web access to wp-content/upgrade (recommended)
The core upgrader touches wp-content/upgrade/ only through local filesystem calls (mkdir, copy, unlink). No legitimate core, plugin, or theme update ever needs a browser or the server itself to make an HTTP request to that directory. Blocking external HTTP access at the web server layer cuts off the attack vector during the transitional update and permanently afterward.
For Nginx, add a high-priority prefix location block inside your server block, before the general .php fastcgi pass directive, then reload Nginx with nginx -s reload:
nginx
location ^~ /wp-content/upgrade/ { deny all; return 403;}
For Apache (VirtualHost or httpd.conf), add a <Directory> directive that blocks all access:
apache
<Directory "/var/www/html/wp-content/upgrade"> Require all denied</Directory>
If you only have .htaccess access, put a RedirectMatch rule at the very top of the root /var/www/html/.htaccess file, above the # BEGIN WordPress rewrite block:
apache
RedirectMatch 403 ^/wp-content/upgrade/
Do not place a protective .htaccess file inside wp-content/upgrade/ itself. Before the legacy updater unpacks a new archive, its cleanup routine wipes the existing contents of that path and deletes your .htaccess right before extraction begins. Confirm that a request to https://your-domain.example/wp-content/upgrade/ returns HTTP 403 Forbidden before you trigger the update.
Option 2: hotfix the upgrader class before updating
For fleets where web server configuration cannot be changed quickly, you can patch the local updater code on disk before running the update. Xint published an audited pre-update patcher that injects the 16-character directory randomization and the upgrade/index.php creation into wp-admin/includes/class-wp-upgrader.php (or wp-admin/includes/update.php on 2.7.x) without breaking core compatibility.
Mind the October 2026 eMudhra intermediate Certificate Authority (CA) key compromise when you fetch external mitigation scripts. Xint’s distribution command uses curl --pinnedpubkey so that hosts with unpatched CA bundles reject any forged TLS certificate along the path:
bash
curl -fsSLO --pinnedpubkey 'sha256//cPNKVMMSBKRsqcDzoa/6g1kBw43KpwPCZYPU38YC1Ig=' https://files.xint.io/wp-patchecho 'db5639db40f02a81ae60ac809c3e79bd365c8797305901e6c8fa30694bd6ad8e wp-patch' | sha256sum -c -sh wp-patch /var/www/html /var/backups/wordpress-pre-update
If you hotfix class-wp-upgrader.php, by script or by hand, reload or restart PHP-FPM or Apache (systemctl reload php8.3-fpm or apache2ctl graceful) before clicking Update Now. With Zend OPcache enabled and opcache.validate_timestamps=0 or a long revalidation interval, the worker pool keeps serving the compiled bytecode of the old, unpatched class from shared memory, which wipes out your disk edit during the update. After the update to 7.1.4 finishes, verify installation integrity with WP-CLI:
bash
wp core verify-checksums --version=7.1.4 --locale=en_US --path=/var/www/html
Option 3: extract the update manually outside the web root
If neither server configuration changes nor a pre-update hotfix is possible, bypass the built-in updater. Download and extract the WordPress 7.1.4 archive in a non-web-accessible directory such as /tmp/, remove its bundled wp-content folder, and sync the core files over the live installation with rsync or cp:
bash
cd /tmpcurl -fsSLO https://wordpress.org/wordpress-7.1.4-no-content.zipunzip -q wordpress-7.1.4-no-content.ziprsync -a /tmp/wordpress/ /var/www/html/wp core update-db --path=/var/www/htmlrm -rf /tmp/wordpress /tmp/wordpress-7.1.4-no-content.zip
The archive is never unpacked inside /var/www/html/wp-content/upgrade/, so setup-config.php is never exposed to the web server in an unconfigured context, and /var/www/html/wp-config.php stays untouched throughout. Sites on end-of-life versions between WordPress 2.7 and 4.6 have no official branch patch. They need to migrate to a supported release using this out-of-band method, along with PHP and MySQL upgrades.
How to detect CVE-2026-119977 exploitation: indicators of compromise
A successful exploit either spawns a working WordPress site inside wp-content/upgrade/ for a short time or permanently re-points the live site’s database. The forensic artifacts differ across web logs, network flows, and the filesystem.
Access logs. Audit historical HTTP access logs (access.log) for any request to a path under /wp-content/upgrade/. Look for bursts of POST requests returning 404, followed by a 200 OK or 302 Found on /wp-content/upgrade/*/wordpress/wp-admin/setup-config.php during a window that coincides with a core update.
Outbound database connections. Inspect firewall logs, NetFlow data, and auditd or eBPF process telemetry for outbound TCP connections on port 3306 (or non-standard high ports) from php-fpm, httpd, or apache2 worker processes to unrecognized external IP addresses. A web server that normally talks only to 127.0.0.1:3306 or an internal RDS subnet has no reason to start an outbound MySQL handshake with an external host.
Filesystem and configuration changes. Check the modification and change timestamps (stat wp-config.php) and the contents of wp-config.php to confirm that DB_HOST, DB_NAME, and DB_USER still point to your legitimate database infrastructure. Audit wp-content/upgrade/ for orphaned directories or rogue .php files. Run wp core verify-checksums and wp plugin verify-checksums --all to catch web shells dropped into wp-content/plugins/, wp-content/mu-plugins/, or root PHP files during a transitional race window.
Database and identity persistence. Query the wp_users and wp_usermeta tables for unauthorized administrator accounts, inspect active session tokens in wp_usermeta (session_tokens), and review installed plugins. If you confirm any indicator of compromise, isolate the host immediately, preserve volatile logs, restore from a verified pre-update snapshot, and rotate all MySQL credentials, WordPress authentication salts (AUTH_KEY, SECURE_AUTH_KEY, and the rest), and any third-party API keys stored in the environment.









