June 14, 2026
Stored XSS via PNG Polyglot Upload and Filename Trust
The Server’s Background Check on Uploaded Files Was Pure Theater
Raghav Raut
4 min read
A few months ago I came across an interesting stored XSS vulnerability while testing a profile picture upload feature on a website that has since been taken offline.
Unfortunately, the site was intentionally shut down before I had an opportunity to responsibly disclose the issue, so this writeup is focused purely on the technical details. All identifying information has been removed.
What made this bug interesting was that the application appeared to validate uploaded files as PNG images, yet I was still able to achieve stored XSS by uploading a PNG polyglot and modifying the filename extension during upload.
At first I thought this was just another file upload bug.
It wasn't.
The more I looked at it, the more it seemed like a mismatch between how the application validated files and how it later stored or served them.
Initial Recon
While exploring the application, I noticed that users could upload a profile picture.
Naturally, the first thing I did was throw random garbage at it because that is apparently how a significant portion of security research begins.
The upload form only accepted image files.
I intercepted the request using Burp Suite and started testing different file types.
Some quick testing produced the following results:
HTML file → rejected TXT file → rejected invalid PNG file → rejected valid PNG file → accepted
This suggested that some kind of content validation was taking place.
Looking at the Upload Request
The upload request contained a multipart filename field.
Something like:
Note: The original request is no longer available because the target application has since been taken offline. The following request is a sanitized reconstruction intended to demonstrate the attack flow.
POST /profile/upload-avatar HTTP/1.1
Host: [redacted]
User-Agent: Mozilla/5.0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryX7Y8Z9
------WebKitFormBoundaryX7Y8Z9
Content-Disposition: form-data; name="avatar"; filename="avatar.png"
Content-Type: image/png
‰PNG
IHDR
...
IDAT
...
[png data]
...
IEND
------WebKitFormBoundaryX7Y8Z9--POST /profile/upload-avatar HTTP/1.1
Host: [redacted]
User-Agent: Mozilla/5.0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryX7Y8Z9
------WebKitFormBoundaryX7Y8Z9
Content-Disposition: form-data; name="avatar"; filename="avatar.png"
Content-Type: image/png
‰PNG
IHDR
...
IDAT
...
[png data]
...
IEND
------WebKitFormBoundaryX7Y8Z9--Instead of uploading a normal PNG, I decided to experiment.
The PNG polyglot
A normal HTML file was rejected.
That meant I needed something that could satisfy image validation while still carrying executable HTML.
The solution was a PNG polyglot.
The file remained a valid PNG image while also containing embedded HTML/JavaScript.
While researching image polyglots, I found a PNG/HTML polyglot that remained a valid PNG image while also containing executable HTML/JavaScript content. I adapted this concept for testing the upload functionality.
When uploaded as a PNG file, it looked like a normal harmless image file.
The Breakthrough
After i got the perfect file for my use, i intercepted the upload request and modified the filename.
Originally:
filename="avatar.png"filename="avatar.png"Modified to:
filename="avatar.html"filename="avatar.html"I intentionally left the multipart Content-Type unchanged:
Content-Type: image/pngContent-Type: image/pngThe upload was accepted.
Reconstructed Request
The original request is no longer available because the application has been taken offline.
The following request is reconstructed from memory and notes.
POST /profile/upload-avatar HTTP/1.1
Host: [redacted]
User-Agent: Mozilla/5.0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryX7Y8Z9
------WebKitFormBoundaryX7Y8Z9
Content-Disposition: form-data; name="avatar"; filename="avatar.html"
Content-Type: image/png
‰PNG
IHDR ĉ sBIT|dˆ 4IDAT™ )ÿÖ <script>alert(document.location);</script>ÈоÛÁ« IEND®B`‚
------WebKitFormBoundaryX7Y8Z9--POST /profile/upload-avatar HTTP/1.1
Host: [redacted]
User-Agent: Mozilla/5.0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryX7Y8Z9
------WebKitFormBoundaryX7Y8Z9
Content-Disposition: form-data; name="avatar"; filename="avatar.html"
Content-Type: image/png
‰PNG
IHDR ĉ sBIT|dˆ 4IDAT™ )ÿÖ <script>alert(document.location);</script>ÈоÛÁ« IEND®B`‚
------WebKitFormBoundaryX7Y8Z9--The important detail is that the uploaded content remained a valid PNG while the filename extension was changed to .html.
Triggering the XSS
After the upload completed, the application generated a URL for the uploaded file.
While the uploaded file looked like an obvious image from the profile page, when opened directly in the browser, the behavior became immediately obvious.
Instead of behaving like a normal image, the browser displayed garbage characters originating from the PNG bytes.
Then the JavaScript executed.
Alert box.
The uploaded file had become active content.
Root Cause Analysis
Based on the observed behavior, the most likely explanation is a validation-storage mismatch.
The application appeared to perform image validation on the uploaded bytes.
However, the storage layer appears to have trusted the attacker-controlled filename extension.
The workflow likely looked something like this:
- Upload file
- Verify file contains valid PNG data
- Accept upload
- Store file using attacker-controlled extension
- Serve uploaded file as HTML
- Browser executes embedded JavaScript
A simple diagram:
Impact
Since arbitrary JavaScript execution was achieved, an attacker could potentially perform actions such as session theft, phishing, credential harvesting, or unauthorized actions on behalf of affected users. The exact impact would depend on the privileges of the user accessing the uploaded file.
Why the PNG Polyglot Was Necessary
One question I asked myself afterward was:
Why not just upload an HTML file?
The answer is simple.
The upload endpoint appeared to validate image content.
Plain HTML files failed.
The PNG polyglot allowed the payload to satisfy image validation while preserving executable HTML content.
Without the PNG component, the upload would most likely have been rejected.
This was the key observation that made the attack possible.
Final Thoughts
What started as routine testing of a profile picture upload eventually turned into a stored XSS vulnerability caused by a mismatch between validation and storage behavior.
The most interesting part was not the JavaScript payload itself.
The interesting part was that the application validated the file as an image while later treating it as active content.
Whenever a system makes two different decisions about the same file, strange things tend to happen.
Sometimes those strange things are called vulnerabilities. lol