Skip to content
Critical Unauthenticated File Upload to RCE in Elementor Pro Plugin

Critical Unauthenticated File Upload to RCE in Elementor Pro Plugin

Patchstack August 19, 2026

This blog post is an unauthenticated arbitrary file upload vulnerability in the Elementor Pro plugin that leads to remote code execution. The flaw lives i

Unauthenticated Arbitrary File Upload to Remote Code Execution

This blog post is an unauthenticated arbitrary file upload vulnerability in the Elementor Pro plugin that leads to remote code execution. The flaw lives in the Forms module’s File Upload field, where the extension check and the file-move step run in two separate loops with different handling of empty file entries. By submitting two file parts for the same field, an unauthenticated attacker skips the extension blocklist entirely and writes a PHP file into a public directory. Patchstack has issued mitigation rules to protect against exploitation of this vulnerability.

This vulnerability was discovered and reported to Patchstack by Tin Pham aka TF1T .

✌️ Our users are protected from this vulnerability. Are yours?

Mitigate vulnerabilities in real-time without changing code.

Identify vulnerabilities in your plugins and get recommendations for fixes.

Protect your users, improve server health and earn additional revenue.

Elementor Pro is the premium extension of Elementor, one of the most widely used WordPress page builders. Among many other features, it adds a Forms widget that lets site owners build , job-application, and support-ticket forms directly in the editor, including a File Upload field so visitors can attach documents.

In version 4.2.1 and below, the plugin’s Forms module handles an uploaded file in two independent steps: it validates the file’s extension in one loop, and it moves the uploaded file to a public directory in a second loop. These two loops treat empty file entries differently, and that mismatch is the whole bug.

The File Upload form field is implemented in modules/forms/fields/upload.php . When a form is submitted, the field’s validation() method walks over the submitted file entries and checks each one’s extension against both an allowed list and a blocklist of disallowed types. Separately, process_field() walks over the same entries and moves each valid upload into the public forms directory.

The problem is that these two loops disagree what to do with an empty file entry (an upload part whose filename is blank, which PHP reports as UPLOAD_ERR_NO_FILE ). The validation loop and the processing loop have different early-exit logic for these empty entries, so a carefully shaped multi-part upload can be seen one way by the validator and another way by the mover.

The intended defense is an extension check. Each uploaded file is validated by is_file_type_valid() , which both requires the extension to be in an allowed list and rejects anything on a hardcoded blocklist:

The blocklist explicitly covers PHP and other executable extensions, so a straightforward .php upload is normally rejected:

That check is sound. It simply never runs. Putting the two loops side by side shows why – the distance between a working defense and an unauthenticated RCE is one keyword:

An empty first entry makes validation() return before it ever type-checks the .php entry that follows it. process_field() , meanwhile, only continue s past that empty entry and moves the .php one anyway. The validator reports a clean submission because it stopped reading; the mover proceeds because it did not.

The move step is the sink. It builds the destination name from PHP’s uniqid() , keeps the attacker’s extension, and moves the file into the public forms directory:

Note that the submitted filename is discarded entirely – only the extension survives into the name the file is stored under. That is why the extension is the whole game here. A classic double-extension attempt like shell.php.jpg is harmless, because it is saved as .jpg , and an uploaded .htaccess is equally inert, because it becomes .htaccess rather than a directory config file. The only thing that matters is getting the final extension past the check, and the loop mismatch does exactly that.

The only prerequisite is that the target site has at least one published Elementor page containing a Form widget with a File Upload field. This is an extremely common, everyday configuration: job-application forms, “attach a photo/ID/receipt” forms, and support-ticket attachments all use it. The field’s “Required” toggle being off is its default state, so no hardened or unusual setting is needed.

Every value the request needs is visible in the public page HTML to any unauthenticated visitor: the post_id , the form_id (Elementor’s internal element id for the Form widget), and the upload field’s form_fields[{FIELD_ID}] input name. The upload is handled through the elementor_pro_forms_send_form AJAX action with no cookies and no nonce required.

The uploaded file is written as wp-content/uploads/elementor/forms/ .php , where is the output of PHP’s uniqid() function (13 hex characters: 8 encode the Unix epoch second, 5 encode the microseconds).

The upload response does not return the file path, so on its own the attacker must recover the uniqid() filename. Because uniqid() is not random but time-based, this is very cheap:

Update Elementor Pro to version 4.2.2 or later. The fix brings the two loops into agreement what an empty file entry means, so an entry can no longer slip past the validator while still being picked up by the mover. Current versions additionally re-check the extension inside process_field() itself, immediately before the file is moved, so the blocklist now guards the sink directly rather than only the validation pass.

Because this vulnerability is unauthenticated and leaves a file behind on disk, updating closes the hole but does not undo an attempt that already succeeded. Sites that ran a vulnerable version with a public File Upload form should also review wp-content/uploads/elementor/forms/ for anything that is not one of the document or image types their forms actually accept, in particular files ending in .php .

Patchstack customers are protected against this vulnerability.

This is a textbook desynchronization flaw: the code that decides whether an upload is allowed and the code that acts on the upload walk the same data with different rules. Neither loop is wrong on its own, and reading either one in isolation shows nothing alarming – the bug exists only in the gap between them. An empty file part is enough to make the validator and the mover disagree, and that disagreement turns a restricted file-upload field into an unauthenticated remote code execution primitive. The root-cause fix is to have both loops the exact same view of which entries are real uploads and which are empty, so an entry can never pass the mover without first passing the validator.

🤝 You can help us make the Internet a safer place

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Protect your users too! Improve server health and earn added revenue with proactive security.

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Extracted Entities