Avatar Upload Turned Into Stored XSS Steals Exchange Users' Identity Data
The Risk
An attacker could send any logged-in user of this crypto exchange a single link that, when opened, quietly read out the victim's real name, registered email, identity-verification status, and their on-chain wallet address, and sent all of it to the attacker. The link sat on one of the exchange's own genuine web addresses, so it looked completely trustworthy and slipped past the usual warning filters. Any registered user could set this up, no special access was needed, and the victim only had to click once. The same link could also rewrite the victim's account name and wipe their active trading session.
The Vulnerability
The finding chains three independent server-side flaws so that a profile picture upload becomes attacker-controlled HTML running on a first-party origin.
1. Unsigned content type on the upload
The avatar upload uses a pre-signed upload URL minted by the application. That signature only covered the request host, leaving the content type attacker-controlled. So a file stored at an image path could be uploaded with a content type of text/html. The file-extension allowlist was not a mitigation: the content delivery layer honored the attacker-supplied text/html even on an accepted .png key, so the stored object served as HTML.
2. Content served as executable HTML
The content delivery host served that stored object as text/html with no X-Content-Type-Options: nosniff, no Content-Security-Policy, and no framing protection. The result executed in the browser as a real first-party page on a genuine subdomain of the exchange.
3. Credentialed wildcard cross-origin policy
The application's data API reflected any subdomain of the parent domain into its allow-origin header while also returning allow-credentials true. The session cookie was scoped to the parent domain with a cross-site setting, so a page running on the content host could make authenticated requests to the data API and read the responses. Any XSS or subdomain takeover anywhere under the parent domain inherited this read capability.
The Attack
Storing the payload
The attacker, using their own account, requested a pre-signed upload URL through the same mechanism the avatar feature uses, confirmed the signed-headers list contained only the host, fetched their HTML payload, and uploaded it to the image path with a text/html content type. The upload returned success and printed a public link on the content host.
Reading the victim's data
When a different logged-in user opened that link, the stored HTML executed on the genuine content origin. Using the victim's own session cookie, it called the data API and read back the victim's profile: registered email and legal name, identity-verification status, on-chain wallet address tied to that identity, account and sub-account topology, sub-account permissions, and referral graph. On the same wildcard-trusted path it could also read the victim's recent session IP addresses, devices, and user agents. All of this was beaconed to an attacker server on a single click. This was demonstrated cross-user: the payload was stored under one account, and when a separate logged-in account opened it, that second user's own data was exfiltrated.
Writes as the victim
Beyond reads, the cookie-authenticated origin could issue writes as the victim. It overwrote the victim's username and legal name and read the new values back to confirm. It could also wipe the victim's active trading session keys, stranding any automation and open-position management until the victim re-authenticated.
The Impact
On a regulated crypto-derivatives exchange this is identity and verification exposure plus deanonymization: the attacker links the victim's real legal name and email to their blockchain wallet address from a single click, with no privilege beyond a normal registered account. The same data-read chain was reachable from any origin under the parent domain because of the credentialed wildcard policy, so it was not confined to one content host.
The impact ceiling is what keeps this High rather than Critical. The chain did not yield full account takeover or fund movement. There was no way to mint a long-lived API key, no way to mint a session key (that required a wallet signature the cookie cannot forge), no password or email change here (authentication was handled by a separate provider), no way to strip multi-factor, and no reach to the trading and funds host (which used a correct narrow origin allowlist). The session cookie itself was not readable. So the impact is bounded to one-click identity, verification, and wallet exfiltration plus identity and session writes.
Remediation
- Include the content type in the signed headers of the pre-signed upload, or validate the stored object's content type server-side and reject anything that is not an image.
- Do not rely on the file-extension allowlist. The delivery layer honored an attacker-supplied
text/htmlon an accepted image extension, so HTML executed from an image key. - Send
X-Content-Type-Options: nosniffand a restrictive Content-Security-Policy on all content-delivery responses so user-uploaded objects cannot execute as pages. - Replace the wildcard subdomain match on the data API with an explicit origin allowlist, as is already correctly done on the trading host, and never return allow-credentials true for content or user-upload origins.