August 30, 2026
Attack and Security Series: Episode 6
When a File Upload Became Remote Code Execution π
By Saurabh Pandey
13 min read
When a File Upload Became Remote Code Execution π
Understanding PHP Internals Through CVE-2026β32475
Welcome back to the Attack and Security Series.
We've gone from:
DRAM β Bots β DNS β AI β Linux Kernel
And now we're coming back up the stack.
This time we're attacking something much closer to the application.
A website.
More specificallyβ¦
PHP.
And if you've ever built a PHP application, WordPress plugin, contact form, or file upload system, this episode is going to be VERY interesting.
Because today's vulnerability starts with something that looks completely harmless: "Please upload your resume."
π
That's it.
A simple file upload.
But behind that tiny button is an entire chain:
Browser
β
HTTP
β
multipart/form-data
β
Web Server
β
PHP
β
$_FILES
β
WordPress
β
Elementor Pro
β
Validation
β
File Movement
β
Public Directory
β
PHP Interpreter
β
RCEBrowser
β
HTTP
β
multipart/form-data
β
Web Server
β
PHP
β
$_FILES
β
WordPress
β
Elementor Pro
β
Validation
β
File Movement
β
Public Directory
β
PHP Interpreter
β
RCEAnd somewhere in the middleβ¦
Two pieces of code disagreed.
That disagreement became:
CVE-2026β32475
Let's go inside.
π§ First Question
What actually happens when I upload a file?
Imagine this HTML:
<form method="POST" enctype="multipart/form-data">
<input type="file" name="resume">
<button type="submit">
Upload
</button>
</form><form method="POST" enctype="multipart/form-data">
<input type="file" name="resume">
<button type="submit">
Upload
</button>
</form>You click:
Choose File
β
resume.pdf
β
UploadChoose File
β
resume.pdf
β
UploadYou might think the browser simply sends:
resume.pdfresume.pdfNope.
The browser constructs an HTTP request.
Something roughly like:
POST /upload HTTP/1.1
Content-Type: multipart/form-dataPOST /upload HTTP/1.1
Content-Type: multipart/form-dataAnd then the body contains multiple parts.
Conceptually:
POST
β
βΌ
multipart/form-data
β
βββ field 1
βββ field 2
βββ file
β
βββ filename
βββ content-type
βββ binary dataPOST
β
βΌ
multipart/form-data
β
βββ field 1
βββ field 2
βββ file
β
βββ filename
βββ content-type
βββ binary dataThis is important.
Because the vulnerability we're studying abuses something that exists inside this multipart structure.
π§© What Is multipart/form-data?
Imagine sending a parcel.
Instead of throwing everything into one box:
BOX
βββ EVERYTHINGBOX
βββ EVERYTHINGyou divide it into sections:
PACKAGE
β
βββ SECTION 1
β name = username
β value = saurabh
β
βββ SECTION 2
β name = email
β value = example@email.com
β
βββ SECTION 3
name = resume
filename = resume.pdf
data = ........PACKAGE
β
βββ SECTION 1
β name = username
β value = saurabh
β
βββ SECTION 2
β name = email
β value = example@email.com
β
βββ SECTION 3
name = resume
filename = resume.pdf
data = ........That's basically what multipart form data does.
The HTTP request has boundaries separating the parts.
------BOUNDARY
field
------BOUNDARY
another field
------BOUNDARY
file
------BOUNDARY--------BOUNDARY
field
------BOUNDARY
another field
------BOUNDARY
file
------BOUNDARY--PHP receives this request and parses it.
And thenβ¦
PHP creates something very familiar.
$_FILES
π Welcome to PHP Internals
PHP gives uploaded files to the application through:
$_FILES$_FILESFor example:
$_FILES['resume']$_FILES['resume']may contain information conceptually like:
name type tmp_name error size
Something like:
$_FILES['resume']
name β resume.pdf
type β application/pdf
tmp_name β /tmp/phpXXXXXX
error β 0
size β 123456$_FILES['resume']
name β resume.pdf
type β application/pdf
tmp_name β /tmp/phpXXXXXX
error β 0
size β 123456And this is where things get interesting.
Because PHP doesn't simply say: "Here's your file."
It gives the application metadata + a temporary file location + an error status.
π₯ The error Field
One of the most important values is:
$_FILES['resume']['error']$_FILES['resume']['error']For a successful upload, PHP normally gives: UPLOAD_ERR_OK
which is: 0
But there are several possible upload errors.
For example:
UPLOAD_ERR_INI_SIZE
UPLOAD_ERR_FORM_SIZE
UPLOAD_ERR_PARTIAL
UPLOAD_ERR_NO_FILE
UPLOAD_ERR_NO_TMP_DIR
UPLOAD_ERR_CANT_WRITE
UPLOAD_ERR_EXTENSIONUPLOAD_ERR_INI_SIZE
UPLOAD_ERR_FORM_SIZE
UPLOAD_ERR_PARTIAL
UPLOAD_ERR_NO_FILE
UPLOAD_ERR_NO_TMP_DIR
UPLOAD_ERR_CANT_WRITE
UPLOAD_ERR_EXTENSIONThe one that matters for today's vulnerability is:
UPLOAD_ERR_NO_FILE
Meaning: There wasn't actually a file uploaded for this particular file input.
So conceptually:
File exists:
name = document.pdf
error = UPLOAD_ERR_OKname = document.pdf
error = UPLOAD_ERR_OKversus:
No file:
name = ""
error = UPLOAD_ERR_NO_FILEname = ""
error = UPLOAD_ERR_NO_FILEAnd that tiny distinction became extremely important.
π§ Now Let's Talk About PHP's Temporary File
When PHP receives an uploaded file, it doesn't normally put it directly into:
/var/www/html/
Instead, PHP first stores it in a temporary location.
Think:
Browser
β
βΌ
HTTP Request
β
βΌ
PHP
β
βΌ
Temporary File
β
βΌ
Application decides where to move itBrowser
β
βΌ
HTTP Request
β
βΌ
PHP
β
βΌ
Temporary File
β
βΌ
Application decides where to move itFor example:
/tmp/phpABC123/tmp/phpABC123The application then decides: "Okay, this file is safe. Let's move it to the upload directory."
This separation is actually a good security design.
The problem occurs when the application makes a mistake while deciding whether the file is safe.
ποΈ WordPress Enters The Picture
Now let's add WordPress.
Our architecture becomes:
INTERNET
β
βΌ
Web Server
β
βΌ
PHP
β
βΌ
WordPress
β
βΌ
Elementor Pro
β
βΌ
Form
β
βΌ
File Upload INTERNET
β
βΌ
Web Server
β
βΌ
PHP
β
βΌ
WordPress
β
βΌ
Elementor Pro
β
βΌ
Form
β
βΌ
File UploadElementor Pro provides a Forms module.
One of its fields is: File Upload
Perfect for:
Resume ID document Screenshot Support attachment Job application
Completely normal functionality.
And that's what makes vulnerabilities like this dangerous.
They hide inside normal functionality.
π What Does Elementor Want To Do?
Imagine the user uploads: resume.pdf
The plugin needs to answer two questions:
Question 1
Is this file allowed?
PDF? YES β
Question 2
Where should it be stored?
uploads/elementor/forms/
So conceptually:
Uploaded File
β
ββββββββββ΄βββββββββ
βΌ βΌ
Validation Storage
β β
βΌ βΌ
Allowed? Move File
β β
ββββββββββ¬βββββββββ
βΌ
Final File Uploaded File
β
ββββββββββ΄βββββββββ
βΌ βΌ
Validation Storage
β β
βΌ βΌ
Allowed? Move File
β β
ββββββββββ¬βββββββββ
βΌ
Final FileLooks perfectly reasonable.
Until we ask: Are those two operations guaranteed to agree about the same file?
π And Here Comes The Bug
Elementor Pro's vulnerable code handled uploaded files using two separate loops.
One loop was responsible for validation.
Another handled processing/moving the files.
Simplified:
foreach ($files as $file) {
validate($file);
}foreach ($files as $file) {
validate($file);
}and later:
foreach ($files as $file) {
move($file);
}foreach ($files as $file) {
move($file);
}Againβ¦
Nothing inherently wrong with having two loops.
The problem was:
They didn't treat an empty upload entry the same way.
And THAT is the vulnerability.
π§ return vs continue
This is one of those tiny programming details that can destroy an entire security boundary.
Let's understand it properly.
Suppose:
foreach ($files as $file) {
if ($empty) {
return;
}
validate($file);
}foreach ($files as $file) {
if ($empty) {
return;
}
validate($file);
}What does:
return;return;do?
It exits the function.
Entire function.
Game over.
Now:
foreach ($files as $file) {
if ($empty) {
continue;
}
move($file);
}foreach ($files as $file) {
if ($empty) {
continue;
}
move($file);
}What does:
continue;continue;do?
It skips this iteration.
Then the loop continues.
So:
returnreturnmeans:
STOP EVERYTHINGSTOP EVERYTHINGwhile:
continuecontinuemeans:
SKIP THIS ONE
NEXTSKIP THIS ONE
NEXTThat difference looks tiny.
But in securityβ¦
Tiny differences can become enormous vulnerabilities.
π₯ The Desynchronization
Let's visualize it.
Suppose the submitted file list is conceptually:
FILE LIST
[0] Empty entry
[1] Another file[0] Empty entry
[1] Another fileThe validation loop sees:
[0]
β
βΌ
EMPTY
β
βΌ
return
β
βΌ
STOP[0]
β
βΌ
EMPTY
β
βΌ
return
β
βΌ
STOPSo the validator never reaches:
[1][1]But the processing loop sees:
[0]
β
βΌ
EMPTY
β
βΌ
continue
β
βΌ
[1]
β
βΌ
PROCESS[0]
β
βΌ
EMPTY
β
βΌ
continue
β
βΌ
[1]
β
βΌ
PROCESSNow we have:
VALIDATOR PROCESSOR
[0] Empty [0] Empty
β β
βΌ βΌ
return continue
β β
βΌ βΌ
STOP [1]
β
βΌ
processVALIDATOR PROCESSOR
[0] Empty [0] Empty
β β
βΌ βΌ
return continue
β β
βΌ βΌ
STOP [1]
β
βΌ
processAnd this is the heart of CVE-2026β32475.
The validator and processor have now developed two different realities.
π» The Security Bug
The security assumption should have been:
IF file passes validation
β
THEN process fileIF file passes validation
β
THEN process fileBut because of the logic mismatch, we effectively get: Validation
File A
β
STOP
File B
β
NEVER VALIDATEDFile A
β
STOP
File B
β
NEVER VALIDATEDwhile:
Processing
File A
β
SKIP
File B
β
PROCESSProcessing
File A
β
SKIP
File B
β
PROCESSTherefore:
UNVALIDATED FILE
β
MOVEDUNVALIDATED FILE
β
MOVEDAnd THAT is the security failure.
π§ͺ Why The PHP Extension Matters
Now let's talk about the dangerous part.
Elementor's validation logic correctly blocked dangerous extensions, including PHP-related extensions.
Conceptually:
Allowed?
β
βββ pdf β YES
βββ jpg β YES
βββ png β YES
β
βββ php β NO βAllowed?
β
βββ pdf β YES
βββ jpg β YES
βββ png β YES
β
βββ php β NO βThe blocklist included PHP and other executable extensions.
Normally:
.php
β
BLOCKED.php
β
BLOCKEDPerfect.
But the vulnerability wasn't: "The PHP blocklist was wrong."
The blocklist was actually doing its job.
The problem was: The dangerous file could reach the processing stage without the validation stage examining it.
That's a much more interesting bug.
π€― Think About It Like Airport Security
Imagine an airport.
Every passenger must pass through:
SECURITY CHECK
β
Allowed?
β
BOARD PLANESECURITY CHECK
β
Allowed?
β
BOARD PLANENow imagine:
Passenger 1
β
Security
β
"I don't have luggage."
β
Security closes entire processPassenger 1
β
Security
β
"I don't have luggage."
β
Security closes entire processBut boarding says:
Passenger 1
β
No luggage
β
Skip
β
Check Passenger 2
β
BOARDPassenger 1
β
No luggage
β
Skip
β
Check Passenger 2
β
BOARDPassenger 2 never went through security.
Yet they still boarded.
That's exactly the kind of logic mismatch we're dealing with.
π
The security guard isn't broken.
The boarding system is.
π Where Does The File Go?
The dangerous part gets worse.
The processing code places the uploaded file in Elementor's forms upload directory:
wp-content/uploads/elementor/forms/wp-content/uploads/elementor/forms/The directory is web-accessible.
So the chain becomes:
Anonymous Visitor
β
βΌ
Elementor Form
β
βΌ
File Upload
β
βΌ
Validation Bypass
β
βΌ
PHP File Written
β
βΌ
Public Directory
β
βΌ
PHP Interpreter
β
βΌ
Remote Code ExecutionAnonymous Visitor
β
βΌ
Elementor Form
β
βΌ
File Upload
β
βΌ
Validation Bypass
β
βΌ
PHP File Written
β
βΌ
Public Directory
β
βΌ
PHP Interpreter
β
βΌ
Remote Code ExecutionPatchstack describes this as an unauthenticated arbitrary file upload leading to RCE.
π But Waitβ¦
Why does a .php file actually execute?
This is where PHP architecture becomes really important.
A .php file sitting somewhere on disk isn't automatically executed.
There has to be a web server configuration that sends PHP requests to a PHP interpreter.
A simplified architecture looks like:
Browser
β
β GET /something.php
βΌ
Web Server
β
β "This is PHP"
βΌ
PHP Handler
β
βΌ
PHP Interpreter
β
βΌ
Execute PHP
β
βΌ
HTTP ResponseBrowser
β
β GET /something.php
βΌ
Web Server
β
β "This is PHP"
βΌ
PHP Handler
β
βΌ
PHP Interpreter
β
βΌ
Execute PHP
β
βΌ
HTTP ResponseHistorically this might involve:
Apache + mod_phpApache + mod_phpor:
Nginx
β
PHP-FPMNginx
β
PHP-FPMModern deployments commonly use PHP-FPM.
βοΈ PHP-FPM
FPM means:
FastCGI Process Manager
Don't let the name scare you.
Think of it as a group of PHP workers waiting for work.
NGINX
β
β FastCGI
βΌ
ββββββββββββ
β PHP-FPM β
β Workers β
ββββββ¬ββββββ
β
βΌ
PHP Interpreter NGINX
β
β FastCGI
βΌ
ββββββββββββ
β PHP-FPM β
β Workers β
ββββββ¬ββββββ
β
βΌ
PHP InterpreterWhen someone requests a PHP resource:
GET /page.phpGET /page.phpthe web server can hand it to PHP-FPM.
PHP executes it.
Then the result goes back:
PHP
β
HTML Response
β
Web Server
β
BrowserPHP
β
HTML Response
β
Web Server
β
BrowserSo if an attacker manages to place executable PHP in a web-accessible directoryβ¦
The file isn't merely stored.
It may become code the server executes.
That's the jump from:
File Upload
to: Remote Code Execution
π₯ Upload Vulnerability vs RCE
This distinction is important.
A file upload vulnerability does NOT automatically mean RCE.
For RCE, several conditions can line up.
For example:
Arbitrary File Upload
β
βΌ
Executable File Type
β
βΌ
File Stored
β
βΌ
Web Accessible
β
βΌ
Server Executes File
β
βΌ
RCEArbitrary File Upload
β
βΌ
Executable File Type
β
βΌ
File Stored
β
βΌ
Web Accessible
β
βΌ
Server Executes File
β
βΌ
RCEIf one link breaks:
PHP execution disabledPHP execution disabledthen:
Upload
β
PHP file
β
No executionUpload
β
PHP file
β
No executionMaybe you still have a serious file-upload vulnerability.
But not necessarily RCE.
This distinction is extremely important during vulnerability analysis.
π¬ Let's Zoom Into The WordPress Layer
Now our stack becomes:
Browser
β
βΌ
HTTP
β
βΌ
Web Server
β
βΌ
PHP
β
βΌ
WordPress
β
βΌ
Elementor Pro
β
βΌ
Forms Module
β
βΌ
Upload Field
β
ββββββββ΄βββββββ
βΌ βΌ
Validation Processing
β β
β βΌ
β File Move
β β
βββββββXβββββββ
β
βΌ
Public Uploads
β
βΌ
PHP Handler
β
βΌ
RCE Browser
β
βΌ
HTTP
β
βΌ
Web Server
β
βΌ
PHP
β
βΌ
WordPress
β
βΌ
Elementor Pro
β
βΌ
Forms Module
β
βΌ
Upload Field
β
ββββββββ΄βββββββ
βΌ βΌ
Validation Processing
β β
β βΌ
β File Move
β β
βββββββXβββββββ
β
βΌ
Public Uploads
β
βΌ
PHP Handler
β
βΌ
RCEThat X is the important part.
The two paths weren't synchronized.
π§ This Is Called A Desynchronization Bug
Patchstack describes the flaw as a desynchronization between the validation and processing logic.
And I really like this concept because you'll see it everywhere in security.
One component believes: "Everything is validated."
Another believes: "Everything here is ready to process."
But the two components don't agree.
That's dangerous.
You can see the same general class of thinking in:
Parser differentials Authentication inconsistencies Proxy confusion Request smuggling Validation bypasses Canonicalization bugs
Different technology.
Same fundamental idea: Two parts of the system interpret the same input differently.
𧨠The Filename Trick That Doesn't Work
Here's another important lesson.
You might immediately think:
shell.php.jpg
Classic upload bypass.
But in this case, Elementor discards the original filename and constructs its own destination name using the extension.
So something like:
shell.php.jpg
ends up being treated according to the final extension:
something.jpg
not:
shell.php.jpg
Patchstack specifically notes that double extensions and .htaccess tricks don't solve the vulnerability.
This teaches an important lesson:
Don't attack based on assumptions.
First understand the actual code path.
𧬠What About uniqid()?
Elementor generates the stored filename using PHP's:
uniqid()
conceptually:
uniqid() + "." + extensionuniqid() + "." + extensionSo:
random-looking-string.php
appears.
But here's an interesting PHP lesson.
uniqid() should NOT be confused with cryptographically secure randomness.
It's primarily time-based.
That means: uniqid()
is not equivalent to: random_bytes()
or a cryptographically secure token generator.
This is another general security lesson: A value looking random does not mean it is random.
Patchstack also discusses how the filename could be recovered because of the time-based nature of uniqid(), including situations where form notification emails reveal the uploaded URL.
π§ Let's Understand move_uploaded_file()
PHP provides:
move_uploaded_file()move_uploaded_file()This function is specifically designed for moving uploaded files from PHP's temporary upload location.
Conceptually:
/tmp/phpXXXX
β
β move_uploaded_file()
βΌ
/uploads/document.pdf/tmp/phpXXXX
β
β move_uploaded_file()
βΌ
/uploads/document.pdfThis is normal.
But remember:
move_uploaded_file()move_uploaded_file()doesn't magically determine whether your business logic is secure.
You still need to validate:
Extension MIME Content Permissions Destination Execution Policy
before deciding what happens next.
A secure primitive can still be used inside insecure logic.
π‘οΈ How Should A Secure Upload System Work?
A better architecture looks like:
FILE UPLOAD
β
βΌ
Authentication
β
βΌ
Authorization
β
βΌ
Size Validation
β
βΌ
Type Validation
β
βΌ
Content Validation
β
βΌ
Filename Generation
β
βΌ
Non-Public Storage
β
βΌ
Malware Scanning
β
βΌ
Store File FILE UPLOAD
β
βΌ
Authentication
β
βΌ
Authorization
β
βΌ
Size Validation
β
βΌ
Type Validation
β
βΌ
Content Validation
β
βΌ
Filename Generation
β
βΌ
Non-Public Storage
β
βΌ
Malware Scanning
β
βΌ
Store FileAnd most importantly:
UPLOADS
β
βΌ
NON-EXECUTABLE DIRECTORYUPLOADS
β
βΌ
NON-EXECUTABLE DIRECTORYDon't put user-controlled uploads somewhere where the web server can execute them as application code.
π Defense Layer 1
Allowlist, Don't Blocklist
Bad philosophy:
Block:
.php
.php3
.phtml
.php5
...Block:
.php
.php3
.phtml
.php5
...Why?
Because executable formats can be complicated.
Better:
Allowed:
.pdf
.png
.jpg
.docx.pdf
.png
.jpg
.docxEverything else: DENY
That's an allowlist.
π Defense Layer 2
Don't Trust the Filename
The user says: resume.pdf
Doesn't mean the content is actually a PDF.
Treat:
filename MIME type extension content
as separate things.
A stronger upload pipeline checks the actual content and uses server-side generated names.
π Defense Layer 3
Store Uploads Outside Executable Web Paths
This is HUGE.
Instead of:
/var/www/html/uploads/
where the server might execute PHPβ¦
prefer an architecture where uploaded content isn't directly executable.
For example:
Application
β
βΌ
Object Storage
β
βββ private
βββ controlled downloadsApplication
β
βΌ
Object Storage
β
βββ private
βββ controlled downloadsor configure the web server so uploaded directories cannot execute server-side scripts.
Then even if validation fails:
Attacker uploads PHP
β
βΌ
File stored
β
βΌ
PHP execution disabled
β
βΌ
RCE chain breaksAttacker uploads PHP
β
βΌ
File stored
β
βΌ
PHP execution disabled
β
βΌ
RCE chain breaksThat's defense in depth.
π Defense Layer 4
Validate Again At The Sink
This is perhaps the biggest lesson from this vulnerability.
Don't assume: validation()
will always happen before: process_field()
If the dangerous operation is: MOVE FILE
then the final processing function should have its own security check.
Conceptually:
Upload
β
βΌ
Validation
β
βΌ
Business Logic
β
βΌ
FINAL CHECK π
β
βΌ
Move Upload
β
βΌ
Validation
β
βΌ
Business Logic
β
βΌ
FINAL CHECK π
β
βΌ
MoveThat way even if an earlier validation layer is accidentally bypassedβ¦
the sink still protects itself.
The patched Elementor version added an additional extension check directly in the processing path, according to Patchstack.
π¨ Defense Layer 5
Patch
Elementor Pro fixed the vulnerability in:
Version 4.2.2
The vulnerable versions are: 4.2.1 and earlier
and the vulnerability is tracked as: CVE-2026β32475
with CVSS: 9.0 β Critical
The patch was released on August 19, 2026.
If you're running Elementor Pro:
UPDATE
β
4.2.2+UPDATE
β
4.2.2+And don't stop there.
If the system was vulnerable before patching, investigate whether suspicious files were already uploaded.
π Incident Response
If you're investigating a potentially affected system, look at:
wp-content/uploads/elementor/forms/
and investigate unexpected executable files.
Also review:
Web Server Logs
β
βΌ
PHP Logs
β
βΌ
WordPress Logs
β
βΌ
Form Submission Activity
β
βΌ
Suspicious FilesWeb Server Logs
β
βΌ
PHP Logs
β
βΌ
WordPress Logs
β
βΌ
Form Submission Activity
β
βΌ
Suspicious FilesThe question isn't only: "Did I patch?"
It's: "Was I already compromised before I patched?"
That's the difference between patch management and incident response.
π§ Let's Reconstruct The Whole Attack Conceptually
Without turning this into a copy-paste exploit, the vulnerability chain looks like this:
UNAUTHENTICATED USER
β
βΌ
Public Form
β
βΌ
Multipart Request
β
βΌ
PHP $_FILES
β
βΌ
Elementor Upload
β
βββββββββββββ΄ββββββββββββ
βΌ βΌ
Validator Processor
β β
Empty entry Empty entry
β β
return continue
β β
βΌ βΌ
STOP Next file
β
βΌ
File processed
β
βΌ
PHP written
β
βΌ
Public web directory
β
βΌ
PHP execution
β
βΌ
RCE UNAUTHENTICATED USER
β
βΌ
Public Form
β
βΌ
Multipart Request
β
βΌ
PHP $_FILES
β
βΌ
Elementor Upload
β
βββββββββββββ΄ββββββββββββ
βΌ βΌ
Validator Processor
β β
Empty entry Empty entry
β β
return continue
β β
βΌ βΌ
STOP Next file
β
βΌ
File processed
β
βΌ
PHP written
β
βΌ
Public web directory
β
βΌ
PHP execution
β
βΌ
RCEThat's the entire vulnerability in one picture.
π€― And Look How Small The Actual Bug Is
This is my favorite part.
The entire disaster doesn't require: 10,000 lines of code
Sometimes the difference between secure and vulnerable code is something as small as: return
versus: continue
One means:
STOP THE FUNCTIONSTOP THE FUNCTIONThe other means:
SKIP THIS ITEMSKIP THIS ITEMWhen you're working with security-sensitive loopsβ¦
You better know exactly which one you mean.
π
π§ͺ What This Teaches Us About Code Review
When reviewing upload code, don't just ask: "Does it check the extension?"
Ask: Where is the check?
Who calls it?
Can the check return early?
What happens to the next item?
Is processing using the same collection?
Does processing revalidate?
Where is the file written?
Can the destination execute code?
Who can reach the endpoint?
Is authentication required?
Is the uploaded content publicly accessible?Who calls it?
Can the check return early?
What happens to the next item?
Is processing using the same collection?
Does processing revalidate?
Where is the file written?
Can the destination execute code?
Who can reach the endpoint?
Is authentication required?
Is the uploaded content publicly accessible?That's a much stronger security review.
π§ The Real Vulnerability Wasn't "PHP Upload"
This is important.
People might summarize CVE-2026β32475 as: "Elementor lets users upload PHP."
That's technically describing the result.
But it doesn't teach you the vulnerability.
The deeper explanation is:
Two independent processing stages
β
βΌ
Different handling of empty entries
β
βΌ
Validation stops early
β
βΌ
Processing continues
β
βΌ
Unvalidated file reaches sink
β
βΌ
Executable file written
β
βΌ
RCETwo independent processing stages
β
βΌ
Different handling of empty entries
β
βΌ
Validation stops early
β
βΌ
Processing continues
β
βΌ
Unvalidated file reaches sink
β
βΌ
Executable file written
β
βΌ
RCEThat's the actual security lesson.
𧬠From PHP To RCE
Let's connect everything we learned.
BROWSER
β
βΌ
HTTP POST
β
βΌ
multipart/form-data
β
βΌ
PHP
β
βΌ
$_FILES
β
βΌ
WordPress
β
βΌ
Elementor Pro
β
βΌ
Upload Field
β
βΌ
Validation
β
X
validation bypass
β
βΌ
Processing
β
βΌ
move_uploaded_file()
β
βΌ
Public Upload Directory
β
βΌ
Web Server
β
βΌ
PHP-FPM
β
βΌ
PHP Interpreter
β
βΌ
RCEBROWSER
β
βΌ
HTTP POST
β
βΌ
multipart/form-data
β
βΌ
PHP
β
βΌ
$_FILES
β
βΌ
WordPress
β
βΌ
Elementor Pro
β
βΌ
Upload Field
β
βΌ
Validation
β
X
validation bypass
β
βΌ
Processing
β
βΌ
move_uploaded_file()
β
βΌ
Public Upload Directory
β
βΌ
Web Server
β
βΌ
PHP-FPM
β
βΌ
PHP Interpreter
β
βΌ
RCEThis is why security researchers don't stop at: "I found a file upload."
They ask: "What can this upload become?"
π° The Castle Analogy
Remember our AI episode?
We talked about trust boundaries.
Same thing here.
Imagine:
CASTLE
β
ββββββββββ΄βββββββββ
βΌ βΌ
Security Storage
Guard Room
β β
βΌ βΌ
Validation File
β
βΌ
Allowed? CASTLE
β
ββββββββββ΄βββββββββ
βΌ βΌ
Security Storage
Guard Room
β β
βΌ βΌ
Validation File
β
βΌ
Allowed?The security guard says: "Everyone has been checked."
But the storage room says:"I'll take whoever comes through."
And the guard accidentally left halfway through.
That's the vulnerability.
The storage room doesn't know: Was this validated?
It simply processes the file.
That's why security checks need to be enforced at security-sensitive boundaries, not merely assumed to have happened somewhere earlier.
π PHP Internals Cheat Sheet
If you're learning PHP for security, remember these:
$_FILES
β
Uploaded file metadata
UPLOAD_ERR_OK
β
Upload succeeded
UPLOAD_ERR_NO_FILE
β
No file uploaded for that entry
tmp_name
β
Temporary uploaded file
name
β
Client-provided filename
type
β
Client/request-reported MIME type
β
Don't blindly trust it
size
β
Reported upload size
move_uploaded_file()
β
Move uploaded file to destination
PHP-FPM
β
PHP process manager
PHP Interpreter
β
Executes PHP code$_FILES
β
Uploaded file metadata
UPLOAD_ERR_OK
β
Upload succeeded
UPLOAD_ERR_NO_FILE
β
No file uploaded for that entry
tmp_name
β
Temporary uploaded file
name
β
Client-provided filename
type
β
Client/request-reported MIME type
β
Don't blindly trust it
size
β
Reported upload size
move_uploaded_file()
β
Move uploaded file to destination
PHP-FPM
β
PHP process manager
PHP Interpreter
β
Executes PHP codeAnd the most important one:
User Input
β
NEVER TRUSTUser Input
β
NEVER TRUSTπ
βοΈ Attack vs Defense
Let's finish the episode with our usual battle map.
ATTACK DEFENSE
Unauthenticated form β Authentication where appropriate
Multipart manipulation β Robust request parsing
Validation bypass β Consistent validation logic
Dangerous extension β Strict allowlist
User-controlled filename β Server-generated filename
Executable upload β Non-executable storage
Public PHP file β Block script execution
Single validation layer β Validate again at sink
Old plugin version β Patch management
Existing compromise β File + log investigationATTACK DEFENSE
Unauthenticated form β Authentication where appropriate
Multipart manipulation β Robust request parsing
Validation bypass β Consistent validation logic
Dangerous extension β Strict allowlist
User-controlled filename β Server-generated filename
Executable upload β Non-executable storage
Public PHP file β Block script execution
Single validation layer β Validate again at sink
Old plugin version β Patch management
Existing compromise β File + log investigationπ Final Lesson
This vulnerability taught me something I didn't expect.
I started thinking: "We're going to learn PHP file uploads."
But we ended up learning:
HTTP
β
Multipart Parsing
β
PHP Internals
β
$_FILES
β
WordPress
β
Plugin Architecture
β
Control Flow
β
Validation
β
File Handling
β
Web Server
β
PHP-FPM
β
Code ExecutionHTTP
β
Multipart Parsing
β
PHP Internals
β
$_FILES
β
WordPress
β
Plugin Architecture
β
Control Flow
β
Validation
β
File Handling
β
Web Server
β
PHP-FPM
β
Code ExecutionThat's cybersecurity.
An attack rarely belongs to one technology.
A vulnerability can begin in: PHP
cross: WordPress
pass through: Elementor
reach: Filesystem
and finally become: Remote Code Execution
That's why I love looking at attacks as chains.
One bug.
Multiple layers.
One final impact.
π» Attack and Security Series β Episode 6 Complete
Episode 1: RowHammer β When Memory Itself Becomes the Attack Surface
Episode 2: Bot Attacks β When Automation Becomes a Weapon
Episode 3: DNS Spoofing β When the Internet Lies About Where You're Going
Episode 4: AI Attack Surface β When the Model Is Only the Beginning
Episode 5: SCTPhantom β The 18-Year-Old Linux Kernel Ghost
Episode 6: Elementor Pro β When a File Upload Became Remote Code Execution
And the funny thing isβ¦
The actual vulnerability wasn't some crazy cryptographic failure.
It wasn't quantum computing.
It wasn't AI.
It wasn't even some massive memory corruption bug.
It was basically: return
versus: continue
Two different interpretations of: "There is no file here."
And somewhere between those two interpretationsβ¦
the security boundary disappeared.
That's the part of cybersecurity I find scary.
Sometimes the biggest vulnerabilities are hiding inside the smallest lines of code.
ππ§π
And that's exactly why we read the internals.