October 13, 2026
MIME-Type Confusion with S3 Bucket Content-Type Override lead to Stored-XSS
Abstract

By D4LT0N
3 min read
This write-up details a vulnerability chain combining an S3 response header override (response-content-type) and MIME-type confusion to achieve Stored XSS via avatar uploads. By leveraging forgotten marketing campaign endpoints that render a custom SPA 404 page, the exploit establishes a stable environment to bypass browser cross-origin restrictions (document.domain) and extract sensitive session tokens from localStorage, ultimately leading to Account Takeover (ATO) or Phishing Attack.
MIME-Type Confusion
During our usual routine of scanning and manual analysis, we zeroed in on the avatar upload functionality. It turned out the application had slack security controls regarding image validation — it only checked surface-level MIME types without actually scanning the uploaded avatars for malicious payloads.
Anyway, the website stores only static assets in an S3 bucket under the static.target.com subdomain. We tried testing whether we could overwrite objects in the S3 bucket, but the server renames the uploaded images using UUIDs.
Amazon Pre-signed URLs & Stored-XSS
When you upload a file to Amazon S3 (Simple Storage Service), you can provide the content type and a variety of other meta data points that Amazon will automatically serve-up when the file is later requested. Sometimes, however, that data was never provided, is inaccurate, or simply needs to be tweaked for the current context. Luckily, Amazon S3 allows you to override certain HTTP response header values in your pre-signed (query string authenticated) URLs [1].
When generating a pre-signed URL for a GET request, you can use specific query string parameters to override the HTTP response headers returned by Amazon S3.
The HTTP response headers that you can override are:
- response-content-type -> Content-Type
- response-content-language -> Content-Language
- response-expires -> Expires
- response-cache-control -> Cache-Control
- response-content-disposition -> Content-Disposition
- response-content-encoding -> Content-Encoding
Ideally, these methods should be strictly restricted to server-side operations, and regular users shouldn't have the ability to manipulate them via arbitrary requests. However, on our target application, we found that we have direct control over the response-content-type parameter — meaning, in short, that we can override how images are rendered and force them into a different format like text/html.
https://static.example.com/a7bc3052-5674-446a-a2cb-0451492a4c96.png?response-content-type=text/htmlhttps://static.example.com/a7bc3052-5674-446a-a2cb-0451492a4c96.png?response-content-type=text/htmlSo, through MIME-type confusion, we can upload an image containing an XSS payload, and then — using the response-content-type control—we can force the browser to render the image as an HTML file instead of a PNG [2] and this lead to Phishing Attack by trusted domain.
Escalate the Impact
So now we have a Stored-XSS, but we are on a subdomain (static.target.com) while the authentication tokens are stored in localStorage on [www.target.com](https://www.target.com) (which is client-side storage and lacks HttpOnly protection).
As we also know, modern browser versions have heavily restricted cross-origin capabilities and document.domain mutations. Therefore, we worked around this by changing the document.domain of static.target.com to target.com, and then leveraged a temporary campaign endpoint with a ready-made 404 status to complete the chain.
Note: it work in Firefox (and maybe for others. didn't test it) not in Chrome anymore.
Why Campaigns Endpoint ?
Campaign endpoints made for KOLs and influencers to track redirects are frequently left behind because developers simply forget to delete the route after the promotional campaign wraps up. These forgotten routes are prime targets for researchers because they usually face fewer security controls, such as relaxed CSP rules or minimal redirection checks. During enumeration, discovering a route with a custom, application-rendered 404 status (instead of the default browser error) confirms that the Single Page Application (SPA) has fully initialized — meaning all core JS files and website assets load successfully, ensuring that localStorage and other client-side data are fully available.
So the XSS payload should open the campaigns endpoint as a child window, then just looking to the readyState and then window.child.localStorage['auth-token'] .
Impact
- Account Takeover in some situation.
- Phishing Attack by trusted domain.
Finally
Special thanks to the C_Bug Security Research Team.