Back to blog

CVE-2026-32475: Unauthenticated File Upload to RCE in Elementor Pro

Elementor Pro 4.2.1 and below let anonymous visitors upload PHP through the Forms file field. Update to 4.2.2 and check your uploads directory.

CVE-2026-32475: Unauthenticated File Upload to RCE in Elementor Pro

Patchstack published CVE-2026-32475 on 19 August 2026: an unauthenticated arbitrary file upload in Elementor Pro that ends in remote code execution. It carries a CVSS score of 9.0, it needs no account, no plugin combination and no unusual configuration, and the only thing a target site has to be doing is running a published page with an Elementor form that accepts file attachments. That is a job-application form, a support-ticket form, a "send us a photo of the damage" form. It is one of the most ordinary things a business website does.

If you run Elementor Pro, the action is short: update to 4.2.2, then look inside your form uploads directory for anything ending in .php. This article covers why the flaw exists, why the usual defences against file-upload attacks do not apply to it, how to tell whether your site was hit before you patched, and what to change so the next bug in this class does not reach the same outcome.

TL;DR

  • CVE-2026-32475 affects Elementor Pro 4.2.1 and below. Fixed in 4.2.2, released 19 August 2026. CVSS 9.0, unauthenticated.
  • The bug is in the Forms module's File Upload field. Extension validation and the file-move step run in two separate loops that disagree about what an empty upload slot means, so the extension blocklist can be skipped while the file still gets written.
  • The result is a .php file in wp-content/uploads/elementor/forms/, a public directory, placed by an anonymous visitor. Requesting it directly executes it.
  • Only sites with a published Elementor form containing a File Upload field are exposed. The free Elementor plugin does not include the Forms widget and is not affected by this issue.
  • Updating closes the hole but does not remove a file that was already uploaded. Check wp-content/uploads/elementor/forms/ for anything that is not a document or image type your forms actually accept.
  • The durable fix, beyond this CVE, is blocking PHP execution inside the uploads directory at the server level.

What CVE-2026-32475 is, in one table

Identifier CVE-2026-32475
Affected software Elementor Pro (commercial plugin)
Vulnerable versions 4.2.1 and below
Patched in 4.2.2
Class Arbitrary file upload leading to remote code execution
Privilege required None (unauthenticated)
CVSS score 9.0
Reported by Tin Pham (TF1T), via Patchstack
Reported to vendor 16 July 2026
Public disclosure 19 August 2026

Two things in that table deserve a second look. The first is unauthenticated. Most WordPress file-upload vulnerabilities need an account of some kind, even a subscriber account, which puts a real obstacle in the attacker's path on a site with registration closed. This one does not. Anyone who can load the page can submit the form.

The second is the month-long gap between the report and the disclosure. That gap is normal and healthy, and it is also why the disclosure date is the date that matters to you. The flaw sat in shipped code from whenever the affected loop logic was introduced until 4.2.2. The window in which someone could plausibly have found it independently is long. The window in which mass scanning becomes likely opens now.

At the time of writing there are no public reports of exploitation in the wild and no public proof-of-concept exploit. That is not reassurance. Recent WordPress history is full of vulnerabilities where the interval between advisory and first blocked attack was measured in hours, and a bug this simple to trigger is a strong candidate for that pattern.

Which sites are actually exposed

The condition is narrow to state and wide in practice: the site must have at least one published page containing an Elementor Form widget with a File Upload field.

That is it. No particular field setting is required. The upload field's "Required" toggle is off by default, and the default state is the exploitable one, so nobody has to have configured anything unusual. If your form has an "attach your CV" field, you are in scope.

A few clarifications that come up immediately:

  • The free Elementor plugin is not affected. The Forms widget is a Pro feature. A site running Elementor alone, without Pro, does not have this code path.
  • Having Elementor Pro installed but no upload field is not exploitable, but it is still worth updating. You may not be the person who built every page on the site, and a form with an upload field can be added by anyone with editing rights at any time.
  • A form behind a login is safer but not automatically safe. The submission endpoint is a public AJAX action; what protects you in that case is that the field and form identifiers a request needs are not published on an anonymous page. Treat that as an obstacle, not a boundary.
  • Elementor's newer Atomic Forms are a separate implementation from the classic Forms module named in the advisory. The advisory points specifically at the classic module's upload field. If your site uses one and not the other, the safe assumption is still "update," because a version number is a much more reliable thing to reason about than which internal module a given page happens to use.

How the bug works: two loops that disagree

This is worth understanding properly, because the mechanism explains why several defences you might assume are protecting you are not.

Elementor Pro's File Upload field does two separate things with a submitted file. One method validates it: it pulls the extension, checks it against the list of types the field is configured to accept, and checks it against a hardcoded blocklist that covers php, phtml, phps, shtml, hta, exe and the rest of the usual suspects. A second method processes it: it takes the file and moves it into the public forms directory.

Both walk the same list of submitted file entries. Both have to decide what to do when they hit an empty entry, meaning an upload slot with a blank filename, which PHP reports as UPLOAD_ERR_NO_FILE.

They made different decisions. The validating loop exits the entire method when it meets an empty entry. The processing loop skips that one entry and carries on.

That difference is the whole vulnerability. Everything after the empty entry is invisible to the validator and fully visible to the mover. The blocklist is correct, comprehensive, and never consulted.

Stripped to its shape, the anti-pattern looks like this:

php
// Validation pass: an empty slot ends the whole check
foreach ( $entries as $entry ) {
    if ( is_empty_upload( $entry ) ) {
        return; // nothing after this point is ever validated
    }
    reject_if_disallowed_extension( $entry );
}

// Processing pass: an empty slot skips one iteration
foreach ( $entries as $entry ) {
    if ( is_empty_upload( $entry ) ) {
        continue; // the loop keeps going, and moves what follows
    }
    move_into_public_directory( $entry );
}

Neither loop is wrong when you read it alone. return on an optional field that was left blank looks entirely reasonable. continue on an empty slot looks entirely reasonable. The defect lives in the space between two functions that were probably written months apart, which is exactly why this class of bug survives code review so often.

Why the usual file-upload tricks are irrelevant here

Here is the part that is genuinely instructive. When the plugin moves an accepted file, it throws away the submitted filename entirely and builds a new one from PHP's uniqid() plus the original extension.

That single design choice neutralises most of the file-upload attack playbook:

  • Double extensions do nothing. A file submitted as shell.php.jpg is stored as <uniqid>.jpg. Inert.
  • Null-byte and path-traversal filenames do nothing. The name never survives.
  • Uploading a .htaccess to re-enable PHP execution does nothing. It is stored as <uniqid>.htaccess, which Apache does not read as a directory config file.

So the extension is the only thing that matters, and the extension check is the only thing that has to be defeated. The loop mismatch defeats it. This is a case where a sound hardening decision (discard the attacker's filename) narrowed the attack surface to a single point, and then that single point failed open.

There is a reason to dwell on this rather than just say "update." If your mental model of file-upload defence is "we block double extensions and we scan MIME types," this vulnerability walks straight past it. The file arriving is a plain .php with no trickery in the name at all.

Why "an attacker cannot find the file" is not a defence

The upload response does not tell the submitter where the file went. It is tempting to treat that as a second line of defence: even if a PHP file lands, nobody can request it without knowing the generated name.

That reasoning does not survive contact with how the name is generated. uniqid() is not random. It is derived from the current time, with the leading portion encoding the Unix second and the remainder encoding microseconds within that second. An attacker who made the request knows roughly when it happened, and your own server tells them precisely when in the Date response header. The remaining search space is small enough to be uninteresting.

There is a shorter path than that, and it is the detail most likely to catch site owners out. Elementor Pro's default email notification renders all submitted fields, and the uploaded file's URL is one of them. On forms that also have a second notification action configured to acknowledge the submitter, that email goes to the address typed into the form. On a job-application or support-ticket form, an autoresponder is a completely normal thing to have set up. In that configuration the site emails the exact URL of the uploaded file to whoever filled in the form.

Obscurity was never the control here. The extension check was, and it was the thing that broke.

What to do now

1. Update to Elementor Pro 4.2.2

This is the fix and everything else is secondary. The patch brings the two loops into agreement about empty entries, and current versions also re-check the extension inside the processing step itself, immediately before the file is moved. That second change is the more important one architecturally: the check now guards the sink directly rather than relying on a separate pass having done its job.

One operational wrinkle specific to commercial plugins. Elementor Pro does not update through wordpress.org. The update is delivered by Elementor's own service and requires an active, connected licence. A site whose licence lapsed six months ago will not be offered the update and will not show a red badge suggesting anything is wrong. If you manage sites for clients, licence expiry is the most likely reason one of them is still on a vulnerable version next week. Check the licence status, not just the plugins screen.

Take a backup first, as with any change to a live site. On MagicWP you can take an on-demand backup before the update and restore it in one click if the release causes a layout regression, which is a real risk with a page builder even on a patch version.

2. Look for what may already be there

Updating stops new uploads. It does nothing about a file placed before you patched. Check:

code
wp-content/uploads/elementor/forms/

You are looking for anything whose extension is not a document or image type your forms actually accept. In particular .php, but also .phtml, .php5, .phar and .hta. Filenames will look like 13 hex characters plus an extension, so nothing will announce itself as suspicious by name.

Over SSH or WP-CLI:

terminal
find wp-content/uploads/elementor/forms/ -type f \
  \( -name '*.php*' -o -name '*.phtml' -o -name '*.hta' -o -name '*.phar' \)

Widen it if you want the fuller picture of what has been submitted to the site:

terminal
find wp-content/uploads/elementor/forms/ -type f -printf '%TY-%Tm-%Td %p\n' | sort

Anything you find should be preserved, not immediately deleted, if you intend to work out what happened. Copy it somewhere outside the web root first.

3. Check whether it was requested

A PHP file sitting in the uploads directory is a loaded weapon. A PHP file in the uploads directory that appears in your access logs with a 200 response has been fired. Search your web server access logs for GET requests to paths under /wp-content/uploads/elementor/forms/, and separately for POST requests carrying the elementor_pro_forms_send_form action, which is the normal form submission endpoint and will also contain the exploit attempts if any occurred.

If your logs only go back a few days, that is a limitation worth knowing about now rather than during an incident.

4. If you find something, treat it as a compromise

File upload to RCE means an attacker who succeeded had the ability to run arbitrary code as your web server user. Removing the uploaded file does not undo that. Look for the standard follow-on artefacts: administrator accounts you do not recognise, unexpected must-use plugins in wp-content/mu-plugins/, modified core or theme files, unfamiliar scheduled events in WP-Cron, and new entries in the options table.

At that point the honest answer is usually to restore from a backup taken before the earliest suspicious timestamp and reapply legitimate changes since, rather than to clean in place. Cleaning a compromised WordPress install by hand is a job that looks finished long before it is.

The change that outlasts this CVE

Every advisory like this one produces the same advice, which is correct and insufficient: update. Here is the thing worth changing afterwards.

PHP should not execute inside the uploads directory. There is no legitimate reason for a .php file in wp-content/uploads/ to run. Nothing in WordPress core, and nothing in a well-behaved plugin, depends on it. If PHP execution is blocked there at the server level, then this vulnerability, and the Royal Elementor upload flaw, and the Metform double-extension flaw, and the next half-dozen of these that get published, all degrade from remote code execution to an attacker having wasted some of your disk space.

On Nginx:

nginx
location ~* ^/wp-content/uploads/.*\.(php|phtml|php[0-9]|phar|hta)$ {
    deny all;
    return 403;
}

On Apache, an .htaccess inside wp-content/uploads/:

apache
<FilesMatch "\.(?i:php|phtml|php[0-9]|phar|hta)$">
    Require all denied
</FilesMatch>

Test it after applying. Upload a harmless PHP file that just prints a string, request it, and confirm you get a 403 rather than the string. Then delete it. A rule you have not tested is a rule you are guessing about, and a typo in a location block can silently match nothing.

Two caveats. First, plugins occasionally write files to uploads subdirectories and expect to execute them; caching and optimisation plugins are the usual offenders. If something breaks after you apply this, that is what happened, and the fix is a narrower rule rather than abandoning the idea. Second, on Nginx this belongs in the server configuration, so it is not something you can apply from inside WordPress; on managed hosting it is a platform-level setting rather than a site-level one. MagicWP applies managed WAF and malware scanning at the platform level across sites, which covers a different part of the problem, and the security features documentation is the place to check what is handled for you before you go and configure it yourself.

What this bug should teach plugin developers

If you write WordPress code that handles uploads, the specific lesson here is more useful than the general one.

The general one, "validate uploads properly," is not actionable, because the developers of Elementor Pro did validate uploads properly. The allowed-list check was there. The blocklist was there and covered the right extensions. The submitted filename was correctly discarded. By the standards of most WordPress form plugins, this was careful code.

The specific one is this: when validation and action are separate passes over the same data, they must agree exactly on what the data is. Any divergence in how the two passes handle an edge case, an empty entry, a null, a duplicate key, an array where a string was expected, is a security boundary that exists only by coincidence. Here the divergence was one keyword.

Two practical rules follow. Validate at the point of action, not only in a separate pass, so that the check cannot be skipped by anything that affects control flow earlier. And be suspicious of return inside a loop that is validating a collection, because it means "stop checking everything" when the author almost always meant "stop checking this one."

Frequently asked questions

Is CVE-2026-32475 being actively exploited?

There were no public reports of in-the-wild exploitation and no public proof-of-concept exploit at the time of writing, one day after disclosure. That is a snapshot, not a forecast. Unauthenticated file upload flaws in widely deployed plugins have historically attracted mass scanning within days of disclosure, and sometimes within hours. Treat the absence of confirmed attacks as time to patch in, not as evidence that patching can wait.

I use Elementor but not Elementor Pro. Am I affected?

No. The Forms widget, and therefore the File Upload field this vulnerability lives in, is a Pro feature. The free Elementor plugin does not contain the affected code. Check which you have on the WordPress plugins screen: the paid plugin appears as a separate entry named Elementor Pro alongside the free one.

My forms do not have a file upload field. Do I still need to update?

You are not exploitable through this specific issue, but yes, update anyway. Upload fields get added to forms by whoever is editing the site, and a version that is safe today because of how the pages happen to be built is safe by accident. Patching on the version number rather than on the configuration is the only approach that survives someone else touching the site.

Will deleting the uploaded file fix a compromised site?

No. If a PHP file was uploaded and requested, the attacker executed code on your server, and anything they did in that window persists after the file is gone. Look for rogue administrator accounts, must-use plugins, modified files and unexpected cron events, and prefer restoring a known-good backup over cleaning in place.

How do I know whether my site was attacked before I patched?

Check wp-content/uploads/elementor/forms/ for files with executable extensions, then search your access logs for requests to that directory and for POST requests to the elementor_pro_forms_send_form action. If the directory is clean and the logs cover the period since 19 August, that is reasonable evidence. If your logs have already rotated away, you have less certainty than you would like, which is itself worth fixing.

Does a web application firewall protect me until I can update?

A WAF with a rule specifically for this issue will block known exploit shapes, and Patchstack has published mitigation rules for its own customers. That is genuinely useful as a stopgap. It is not equivalent to patching, because a generic rule can be evaded by a request that differs from the pattern it was written against. Use it to buy hours, not weeks.

Why did the vendor patch take a month to ship?

The researcher reported the flaw on 16 July, the vendor had a patch ready the following day, Patchstack confirmed the fix in early August, and 4.2.2 shipped on 19 August alongside the advisory. Coordinated disclosure timelines like this are deliberate: the details stay private until users have a version to move to. The practical consequence for you is that the clock on public risk starts at disclosure, not at discovery.

Conclusion

CVE-2026-32475 is the kind of vulnerability that punishes ordinary configurations. There is no exotic plugin combination, no unusual setting and no account required. A published page with a file upload field, which is one of the most common things an Elementor site has, was enough for an anonymous visitor to place executable PHP in a public directory.

Update Elementor Pro to 4.2.2, check the licence status on any site you manage that has not offered you the update, and look through wp-content/uploads/elementor/forms/ before you consider the job done. Then do the thing that pays off across every future advisory in this category and stop PHP executing inside your uploads directory at all.

If keeping track of plugin versions, licence expiry and post-incident cleanup across a portfolio of sites is more work than it is worth, that is a large part of what managed hosting exists to absorb. MagicWP runs daily off-site backups with one-click restore, managed WAF and malware scanning on isolated containers, so that when the next unauthenticated upload flaw lands you are checking one directory and restoring one backup rather than rebuilding a site from memory.

A
Alex
MagicWP
Writing about WordPress, performance, and the infrastructure that makes sites fast.

Get the best of MagicWP in your inbox.

Monthly engineering notes, product updates, and WordPress performance tips. No spam, unsubscribe anytime.

Join 12,000+ builders. We send one email a month.