Automated Threat Modeling

Overview of CVE-2026-119977: WordPress Core Upgrade Window Race Condition Enabling Unauthenticated RCE

A brief summary of CVE-2026-119977, a race condition in WordPress Core 4.7 through 7.1.3 where predictable, web-accessible staging paths used during full-package upgrades can allow unauthenticated code execution. Covers the root cause, the 7.1.4 patch and branch backports, and published detection guidance.

CVE Analysis

14 min read

Gaurav Sarraf
Gaurav Sarraf

2026-11-03

Overview of CVE-2026-119977: WordPress Core Upgrade Window Race Condition Enabling Unauthenticated RCE
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

For a few seconds during every full-package WordPress Core upgrade on versions 4.7 through 7.1.3, a complete, unconfigured copy of WordPress sat at a predictable URL on the live web server, reachable by anyone. Within nine minutes of Wordfence shipping a firewall rule for CVE-2026-119977, it was logging requests aimed at those staged installer paths, and some sources polled the same sites for roughly 14.5 hours waiting for an update to begin.

This post is a brief summary of what the public advisories, the 7.1.4 patch, and published hunting guidance tell us about the flaw. The attack surface is the maintenance process itself. The window of exposure is the upgrade, and the remedy is also an upgrade.

Technical information

Classification and severity

NVD classifies the weakness as CWE-367, a time-of-check to time-of-use race condition. The CVSS 3.1 vector, assigned by Wordfence as the CNA, is:

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

That yields 8.1 High: network reachable, high attack complexity, no privileges, no user interaction, and high impact on confidentiality, integrity, and availability. NVD shows no separate NIST assessment. The WordPress project advisory (GHSA-4w3m-c66w-69mg) labels the issue Low, while still acknowledging that remote code execution is possible if an attacker wins the extraction race. In our reading, the two ratings weigh the same facts differently: the timing requirement on one side, the consequence on the other. The timing constraint limits exploitation, but it does not reduce the impact when an attack succeeds.

Root cause: a staging directory named after the package

Before the fix, WP_Upgrader::unpack_package() built its working directory from the upgrade folder plus the package basename, after stripping any .tmp or .zip suffix:

$working_dir = $upgrade_folder . basename( basename( $package, '.tmp' ), '.zip' );

That directory lives inside wp-content/upgrade/, which the web server serves. During a full-package Core upgrade, the entire WordPress release is extracted there, including installer files such as wp-admin/setup-config.php. Anyone who knew the package name could therefore predict the full URL of the extracted WordPress tree while the upgrade was running.

Two further behaviors in the vulnerable code made the situation worse:

  • The listing guard was deleted. Before unpacking, the upgrader cleaned out everything in the upgrade folder, including its index.php placeholder. On servers with directory listing enabled, a request to /wp-content/upgrade/ could reveal the staging folder name even without predicting it.
  • Staged configuration was promoted. In vulnerable versions, update_core() copied files from the working directory into the live site root, skipping only wp-content and wp-includes/version.php. A wp-config.php that appeared in the staging tree would be copied along with the rest of the release.

Attack flow

Based on the advisories, the patch analysis, and Wordfence telemetry, the sequence looks like this:

  1. An administrator or a background update starts a full-package Core upgrade. WordPress extracts the release into a predictable directory beneath wp-content/upgrade/.
  2. An unauthenticated attacker, who has been polling the predicted path (or who read the folder name from a directory index), requests the staged setup-config.php installer while extraction and upgrade processing are underway.
  3. If the web server executes PHP in that directory, the staged installer runs. It loads WordPress from its own copied files, so security plugins running inside the live site never see the request.
  4. According to the patch analysis, an attacker who reaches the staged installer could write a wp-config.php into the working directory. In vulnerable versions, update_core() would then copy that file into the live site root. The advisories describe the end result as PHP code execution on the server.

Prerequisites and open questions

Exploitation is conditional. Sending requests to an affected site is not enough on its own. The documented prerequisites are:

  • A full-package upgrade whose package contains installer files.
  • A staging directory that is web accessible with PHP processing available.
  • The attacker winning the race during the extraction window.

The sources do not establish the precise race duration, a reproducible exploit request sequence, or the operating system privilege level gained. The advisories describe full-package upgrades, and the evidence does not extend the same exploit claim to every partial update mechanism.

Disclosure timeline

  • October 6, 2026: WordPress 7.1.3 ships with 7 security fixes and 4 bug fixes. It remains within this CVE's affected range.
  • October 28, 2026: WordPress 7.1.4 ships. Wordfence publishes its entry crediting Umut Bayram of Picus Security and deploys a firewall rule.
  • October 29, 2026: WordPress project advisory published with a Low severity label.
  • October 31, 2026, 06:16:34.550: NVD publication (source: [email protected]).
  • November 2, 2026: NVD record modified.

NVD publication should not be treated as the start of public awareness.

Patch information

WordPress fixed the bug in Trac changeset 64877, titled "Upgrade/Install: Improve the handling of the upgrade working directory." It was merged into the 7.1 branch as commit e7b33f536d308e9656442677edefdcfe1d4fa130 (SVN r64889) and shipped in WordPress 7.1.4 on October 28, 2026. The commit credits umut3806, marcs0h, rfaile313 and jorbin. It touches only src/wp-admin/includes/class-wp-upgrader.php and src/wp-admin/includes/update-core.php, adding 36 lines and removing 4. The fix leaves staging inside the web root and adds three layers instead.

Layer 1: an unguessable staging directory name

A new private helper keeps the old base name but appends 16 random characters:

private function get_working_dir_name( $package ) { // Strip off any .tmp or .zip suffixes. $name = basename( basename( $package, '.tmp' ), '.zip' ); return $name . '-' . wp_generate_password( 16, false ); }

Passing false to wp_generate_password() restricts the suffix to letters and digits, which keeps the name filesystem safe while still leaving 62^16 possible suffixes. unpack_package() now builds $working_dir from this helper. Because the method lives in the base WP_Upgrader class, the random name applies wherever that class unpacks a package, not only to Core updates.

Layer 2: no directory listing for the upgrade folder

A random name only helps if the parent folder cannot be listed. The patch creates wp-content/upgrade/ if needed and ensures an index.php placeholder exists, mirroring the guard Core already uses for wp-content, plugins and themes:

/* * The upgrade directory is web-served. Keep it from being listed, as core does * for wp-content and its plugins and themes directories, so the name of the * working directory created below cannot be discovered from a directory index. */ if ( ! $wp_filesystem->is_dir( $upgrade_folder ) ) { $wp_filesystem->mkdir( $upgrade_folder, FS_CHMOD_DIR ); }

The cleanup loop that runs before unpacking now skips the guard file so it is never removed:

// Clean up contents of upgrade directory beforehand, keeping its index.php. if ( 'index.php' === $file['name'] ) { continue; }

Layer 3: never promote a staged wp-config.php

The final change limits damage if the race is won anyway. The skip list in update_core() previously read:

$skip = array( 'wp-content', 'wp-includes/version.php' );

In 7.1.4 it reads:

$skip = array( 'wp-content', 'wp-includes/version.php', 'wp-config.php' );

The accompanying docblock states: "Never copy a wp-config.php, which could have been written to the working directory." A wp-config.php planted in staging can no longer replace the site's real configuration.

How the layers combine

  1. The random name means attackers cannot predict the path.
  2. The index.php guard means they cannot find it by listing the folder.
  3. The wp-config.php skip means anything they write to staging never reaches the live site.

An attacker would have to defeat all three to repeat the original attack.

Backports and update guidance

Advisory GHSA-4w3m-c66w-69mg states the fix was backported to every branch back to 4.7. On older branches it arrives bundled with the other 7.1.4 security fixes. For example, the 5.9 branch commit ea6e6d4082ae564fe8ee1c332dc27ba5f4a8fdae merges [64873], [64874], [64876], [64877] and [64879], with get_working_dir_name(), the index.php guard and the $skip change matching the 7.1 versions apart from context. The 7.1.4 announcement also notes the fixes are included in WordPress 7.2 Beta 3.

WordPress recommends updating immediately: download 7.1.4 from WordPress.org, or open the dashboard, select Updates, then Update Now. Sites supporting automatic background updates begin the process automatically. There is an obvious tension here, since the vulnerable activity is itself an upgrade, but the sources offer no reason to postpone patching, and no standalone Apache, Nginx, or firewall configuration workaround is prescribed for this CVE. For sites pinned to older branches, note that WordPress actively supports only the latest version and describes older backports as a courtesy. Receiving this backport is not a promise of indefinite maintenance for that branch.

Detection methods

The attack only works in a short, predictable window: the attacker must request a PHP file, usually the staged setup-config.php, from a temporary folder under wp-content/upgrade/ during the seconds a full-package Core update spends unpacking. That gives defenders three places to look: web requests to that folder, the exact time each site upgraded, and what changed on the host afterwards.

What attack traffic looks like

Wordfence deployed a firewall rule on October 28, 2026, logged its first match nine minutes later, and recorded more than 18,500 requests aimed at staged setup-config.php installer paths in the first 21 hours. Two of the largest sources polled the same small groups of sites for about 14.5 hours. Others cycled through many staging folder and version name variations. Two profiles are worth separating:

  • Scanners try many different paths across many sites.
  • Patient pollers request one or a few exact paths against a site, every few seconds or minutes, for hours. These were waiting for that site's update window.

As of November 2, neither Wordfence nor Patchstack had published attacker IPs, user agents, or webshell names, so hunting has to rely on request path and timing rather than atomic indicators.

Searching access, CDN and WAF logs

Normal visitors never request PHP files under wp-content/upgrade/. Treat every hit as worth reviewing and a 200 response as a priority. ThreatDefence published the hunts below. Start the window at October 28, 2026, and widen it to cover each site's actual upgrade time if it updated later.

Raw combined-format access logs:

grep -E '"(GET|POST) /wp-content/upgrade/[^ ]*\.php' /var/log/nginx/access.log* /var/log/apache2/access.log* 2>/dev/null \ | awk '{print $1, $4, $7, $9}' | sort | uniq -c | sort -rn | head -50

Splunk:

index=web (uri_path="/wp-content/upgrade/*" OR uri="*/wp-content/upgrade/*") | stats count min(_time) as first max(_time) as last values(status) as status values(uri_path) as paths by src_ip, site | sort - count

The next hunt targets the installer itself: setup-config.php or install.php anywhere other than the normal wp-admin location, plus any POST to them. A POST to a staged setup-config.php that returned 200 means someone reached the installer. Treat that as a probable compromise.

grep -E '(setup-config|install)\.php' /var/log/nginx/access.log* | grep -v '"GET /wp-admin/install.php' | grep -E 'wp-content/upgrade|POST'

Also look for requests to the bare /wp-content/upgrade/ folder. Because vulnerable cleanup deleted the index.php guard, a 200 for that folder on a server with directory listing enabled may have revealed the staging folder name.

For Microsoft Sentinel with Azure Front Door or Application Gateway WAF logs, this query labels each source as sustained polling or scanning based on distinct paths and active hours. Adapt field names to your log source and test it first:

AzureDiagnostics | where TimeGenerated > datetime(2026-10-28) | where requestUri_s has "/wp-content/upgrade/" | extend hour = bin(TimeGenerated, 1h) | summarize requests = count(), distinctPaths = dcount(requestUri_s), activeHours = dcount(hour), first = min(TimeGenerated), last = max(TimeGenerated) by clientIP_s, host_s | extend pattern = iff(distinctPaths <= 3 and activeHours >= 4, "sustained polling", "scanning") | order by activeHours desc, requests desc

A 404 on a staged path is good news, because the folder did not exist when that request arrived. The requests to investigate are those that received a 200 or reached PHP while an update was running.

Retrospective hunt anchored to each site's upgrade

Since exploitation only succeeds while an update is unpacking, the most useful hunt is tied to each site's own upgrade time:

  1. Find when the site updated. The modification time of wp-includes/version.php is a good marker (stat -c '%y' wp-includes/version.php). Background updates also leave a wp-cron.php request around that time. Manual updates show wp-admin/update-core.php?action=do-core-upgrade.
  2. Pull every request from 5 minutes before to 10 minutes after that moment, looking for requests to wp-content/upgrade, installer files, unfamiliar PHP filenames, and unusual POST requests.
  3. Repeat for earlier full-package updates still within log retention if the site was visible to scanners. The flaw dates back to at least 4.7.

Plugin logs have a blind spot here. The staged installer loads WordPress from its own copied files, so a security plugin running inside WordPress never sees the request. Wordfence notes its firewall only catches it when Extended Protection is enabled, which makes it run before direct PHP requests. Rely on web server, CDN, or WAF logs rather than plugin logs.

Signs of compromise on the host

Wordfence and ThreatDefence suggest the following checks:

# Leftover files in the upgrade folder (should hold only index.php outside a running update) find wp-content/upgrade -mindepth 1 ! -name index.php # Stray PHP files find wp-content/uploads wp-content/upgrade -name '*.php' -newermt '2026-10-27' # New administrator accounts wp user list --role=administrator --fields=ID,user_login,user_email,user_registered # Tampered files wp core verify-checksums wp plugin verify-checksums --all

Failed updates can also leave folders behind in wp-content/upgrade, so check contents and timestamps before drawing conclusions. Compare administrator accounts against your approved list, look for unfamiliar plugins or themes installed around the upgrade time, and check for changes to wp-config.php, .htaccess and wp-content/mu-plugins. Wordfence recommends running a scan after updating to catch unexpected Core changes and malicious files.

On the process side, watch for PHP FPM or Apache workers starting sh, bash, curl, wget or python, or making unexplained outbound connections. ThreatDefence's Microsoft Defender for Endpoint hunt for Linux hosts filters out sendmail, since PHP's mail() legitimately starts a shell:

DeviceProcessEvents | where Timestamp > datetime(2026-10-28) | where InitiatingProcessFileName startswith "php" or InitiatingProcessFileName in~ ("apache2", "httpd", "nginx", "lsphp", "litespeed") | where FileName in~ ("sh", "bash", "dash", "curl", "wget", "python", "python3", "perl", "nc", "ncat", "base64") | where ProcessCommandLine !has "sendmail" | project Timestamp, DeviceName, AccountName, InitiatingProcessFileName, InitiatingProcessCommandLine, FileName, ProcessCommandLine | order by Timestamp asc

The same source provides a DeviceFileEvents hunt for .php or .htaccess files created or changed under wp-content/upgrade/, uploads/ or mu-plugins/. A legitimate update writes thousands of files under upgrade/ in a burst lasting seconds, so focus on files written outside that burst and on anything appearing in uploads or mu-plugins.

Vendor signatures and scanner checks

The following commercial detections exist, though their rule logic is not published:

  • Trend Micro / TrendAI (advisory KA-0024925): Deep Security/SWP IPS rule 1012774 (detect mode, on by default); TippingPoint DV filter 48746, "HTTP: WordPress Core Insecure Upgrade Race Condition Vulnerability" (prevent mode); Network Detection and Response rule 6045; Vision One endpoint indicators for upgrade directory file enumeration and for setup-config.php modification inside the upgrade folder, plus XDR workbenches such as "PHP based Command Execution Attack".
  • Qualys: QID 154198 detects vulnerable versions, according to Feedly's timeline.
  • runZero: a published software inventory query matching every vulnerable range, useful for fleet-wide exposure checks.
  • Manual version check:
wp core version find / -path '*/wp-includes/version.php' -exec grep -H "wp_version =" {} \; 2>/dev/null

No public YARA, Sigma, Snort, or Suricata rule specific to this CVE was found.

Do not mix in wp2shell indicators

Indicators from the July 2026 "wp2shell" chain (CVE-2026-63030 / CVE-2026-60137), such as POST /?rest_route=/batch/v1, User-Agent: wp2shell and wp2shell_<hex> plugin folders, belong to a different REST batch endpoint bug and will not identify upgrade-window race attempts. The only overlap is the generic signal of a web server spawning a shell.

A suspicious request to wp-content/upgrade/ justifies investigation but does not prove compromise. Confirm by matching a successful response during the upgrade window with host-side evidence.

Affected systems and versions

Affected: WordPress Core 4.7 through 7.1.3. Per runZero's inventory query, the vulnerable ranges run from 7.1.0 through 7.1.3 and 7.0.0 through 7.0.7, down to 4.7.0 through 4.7.38 on the oldest branch.

Fixed releases by branch (versions at or above these are patched for this CVE, even if numerically lower than 7.1.4):

  • 7.1: 7.1.4
  • 7.0: 7.0.8
  • 6.9: 6.9.11
  • 6.8: 6.8.12
  • 6.7: 6.7.11
  • 6.6: 6.6.11
  • 6.5: 6.5.14
  • 6.4: 6.4.14
  • 6.3: 6.3.14
  • 6.2: 6.2.15
  • 6.1: 6.1.16
  • 6.0: 6.0.18
  • 5.9: 5.9.20
  • 5.8: 5.8.19
  • 5.7: 5.7.21
  • 5.6: 5.6.23
  • 5.5: 5.5.24
  • 5.4: 5.4.25
  • 5.3: 5.3.27
  • 5.2: 5.2.30
  • 5.1: 5.1.28
  • 5.0: 5.0.31
  • 4.9: 4.9.35
  • 4.8: 4.8.34
  • 4.7: 4.7.39

The fixes are also included in WordPress 7.2 Beta 3.

Vulnerable configuration: a full-package Core upgrade on a server where wp-content/upgrade/ is web accessible and PHP executes there. Directory listing enabled on that folder made the staging name discoverable without prediction.

Vendor security history

WordPress is an open-source project created in 2003 by Mike Little and Matt Mullenweg as a fork of b2/cafelog, licensed GPLv2 or later. W3Techs reports it at 58.3% of websites whose CMS is known (October 29, 2026). WordPress itself cites more than 43% of the web. The denominators differ, and neither figure measures how many installations meet this exploit's staging and timing prerequisites.

The project describes a Security Team of more than 50 trusted experts, code review by trusted committers, and development of fixes and test cases before security releases, with private reports routed through HackerOne. The 7.1.3 release on October 6, 2026 contained 7 security fixes and 4 bug fixes, yet still fell inside this CVE's affected range. A security release addresses known issues without guaranteeing the absence of others. There is also a support nuance: the repository's SECURITY.md lists 4.7.x through 7.1.x as supported, while the release announcement and general security policy say only the latest version is actively supported, with older fixes provided as a courtesy.

For ecosystem context, Patchstack's 2026 report (covering 2025) counted 11,334 new WordPress ecosystem vulnerabilities, 42% more than 2024. Plugins accounted for 91% and themes for 9%, while Core had 6 reported vulnerabilities, described as low priority. Those historical figures do not determine this CVE's severity, but they do show Core issues are a small slice of overall WordPress vulnerability volume.

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Gerrit
  • CVS
  • Jira
  • Linear
  • Slack
  • Security Compass
Explore integrations
Security magnifying glass visualization