Introduction
During a recent penetration test, I discovered a stored Cross-Site Scripting (XSS) vulnerability tied to a file upload feature. What initially appeared to be a simple content upload mechanism turned out to be a serious attack vector that could result in arbitrary script execution in the application's user context. In this post, I'll break down how the issue was identified, why it was exploitable, and how it should be fixed.
Background
The application had a functionality where it allowed users to submit information about a defected purchase order. This also required the user to upload a pdf file with order details. These uploaded files were stored on the server side.
Discovery
It wasn't late before I figured out a way to bypass client-side checks. I found out that I could upload .pdf and .txt files by using a proxy tool to modify the request itself.


During my previous testing, I had discovered that the application was built using php. This new information got me excited about discovering an RCE. I began trying to get a webshell by uploading php files. However, the php code just won't execute on the server side(maybe an EDR was doing it's job well). This greatly shattered my hopes and dreams. Alright, I will admit is, a bit too dramatic, but it did break my spirit a little. After a lunchbreak of 20 mins and a 15 minute walk, I resumed my testing with a clear head. Just out of curiosity, I uploaded an .html file to see if it executes and…

Behind the Scenes
I asked myself this question — Why did XSS occur in this case when it was nowhere else to be found?
The rest of the site seemed secure: sanitization and encoding were applied everywhere input eventually got reflected in the UI. That's why normal XSS didn't trigger in text fields or form inputs.
But when I uploaded a file and then navigated directly to its URL, something different happened. The server didn't treat the file as "data to sanitize" — it just served it back as content. Since the uploaded file was delivered as an HTML document that the browser could parse, the browser treated it like any normal page and ran the <script> inside it. That's exactly how stored XSS via file upload works in practice: the attack isn't coming from reflected input in templates, it's coming from content that the server blindly serves as HTML to a user's browser.
In short: everything else was safe because it was sanitized before rendering. But the uploaded file was served straight up as an HTML document the browser was happy to interpret and execute.
Why This Is Serious
Stored XSS isn't just a harmless alert box — it's one of the most dangerous client-side vulnerabilities because the malicious code is persistently stored and automatically executed in the browser of any user who views the affected content. Unlike reflected XSS, it doesn't require tricking a victim into clicking a crafted link; normal use of the application is enough to trigger the exploit.
Responsible Fixes
Implementing server-side validation(for any input that comes from user) is an absolute necessity as client-side validation can easily be bypassed.
The following checks are must-to-have: -
- Checking the file name for special characters and double extensions.
- Checking the Content-Type header of the incoming upload request.
- Checking the content for magic byte of the uploaded file.
- Checking the actual content of the file.
You can also go a step further and implement AI-based detection.
Why so many checks? Because...

This was not an RCE, but was equally interesting.