Zero-Click Stored XSS in Chat: When “Just Open the Window” Is Enough

Severity: High
Bounty Awarded: $394
Program: Private Bug Bounty
Platform: Bugbounty.sa
Most chat XSS bugs are noisy.
You send a payload.
The victim has to click something.
Refresh the page.
Open the message.
Maybe click a link.
Sometimes the payload only executes in specific render paths.
This one was different.
The target was a gaming/social platform with a built-in friend messaging system under:
https://talk.redacted.gg/talk
At first glance, the feature looked standard:
add friends
open conversations
exchange direct messages
Nothing unusual.
The interesting part came from how chat messages were rendered.
Because simply having the chat window open was enough for code execution.
No click required.
No refresh required.
No interaction required.
Recon: Testing Chat Rendering
Whenever I test messaging systems, I usually start with one question:
How are messages rendered?
Specifically:
Is HTML escaped?
Is rich text supported?
Is sanitization server-side or client-side?
Are messages inserted into the DOM safely?
Chat systems break surprisingly often because developers try to allow “rich” formatting while only partially sanitizing user input.
I started with harmless HTML:
<b>hello</b>
Then basic render probes:
<img src=x>
and malformed HTML to see how the parser behaved.
Very quickly, it became obvious:
Some HTML was surviving.
But not everything.
Common payloads failed.
For example:
<script>alert(1)</script>
was blocked or sanitized.
Certain event handlers were stripped.
Classic beginner payloads did not work.
That usually means:
Keep digging.
Partial filtering is often where the best XSS lives.
Because developers blacklist obvious payloads but miss alternate execution primitives.
Message Flow Analysis
The platform exposed a fairly standard chat API.
When sending a message, the request looked roughly like:
POST /api/chat/send HTTP/2
Host: talk.redacted.gg
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.redacted.signature
Content-Type: application/json
Cookie: session_token=sess_84f91abf77f2
{
"conversationId": "92831",
"message": "<payload>"
}
Successful response:
{
"success": true,
"message": {
"id": 338192,
"body": "<payload>",
"created_at": "2023-03-24T02:18:49Z"
}
}
The important observation:
The server stored message content with insufficient sanitization.
Meaning:
attacker sends message
↓
message stored
↓
recipient chat loads
↓
message rendered in DOM
↓
code execution
At this stage, the question changed from:
“Can I inject HTML?”
to:
“What execution primitive survives filtering?”
Finding the Right Primitive
After iterating through different HTML rendering behaviors, I found a payload structure that consistently survived filtering and executed in the chat body.
The platform filtered obvious payloads:
Examples:
<script>alert(1)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
Which initially made it look reasonably protected.
But the filtering turned out to be inconsistent.
Some tags were stripped.
Some attributes were filtered.
Others passed through untouched.
That inconsistency usually means one thing:
There is probably an alternate parser path.
After iterating through different HTML rendering behaviors, I found a payload structure that consistently survived filtering and executed in the chat body.
The payload class that worked relied on an iframe-based JavaScript execution context:
<IFRAME SRC="javascript:window['al'+'ert']('[redacted]')"></IFRAME>
The payload survived filtering because:
the renderer trusted iframe tags
the
javascript:context was not blockedsimple keyword filtering could be bypassed
message content was rendered directly into the DOM
The important takeaway was not the payload itself.
It was the filtering model.
The application attempted to block obvious XSS signatures, but relied on incomplete filtering rather than robust sanitization.
Which turned:
“XSS filtered”
into:
“XSS filtered unless you slightly change the primitive”
The key issue was architectural:
The renderer trusted HTML that should never have been trusted in a real-time chat context.
And because messages were persisted:
The XSS became stored.
Not reflected.
Not self-XSS.
Not DOM-only.
Persistent.
The Part That Changed Severity: Zero-Click Execution
Most stored XSS still requires something from the victim.
Examples:
opening a message
refreshing the page
clicking content
interacting with notifications
This one behaved differently.
If the victim already had the chat open:
attacker sends message
↓
websocket/event received
↓
message inserted into DOM
↓
payload executes immediately
Observed behavior:
no refresh needed
no clicking required
no reopening conversation
no user interaction at all
Just:
Chat window open = code execution
That dramatically changes exploitability.
Because now the attacker does not need:
victim curiosity
Only:
victim presence
And on active gaming/social platforms, users often leave chat open in the background.
Which made exploitation extremely realistic.
At this point, the bug already looked solid.
But another detail pushed the impact much higher.
The messaging system exposed reachable internal/staff accounts.
And that changed the threat model completely.
Staff Reachability: Why the Threat Model Changed
At this point, the stored XSS already looked strong:
persistent payload
zero-click execution
realistic delivery path
no user interaction required
That is already a serious bug.
But the thing that changed severity for me was simple:
Staff accounts were reachable through normal chat functionality.
While testing conversations, I noticed accounts associated with platform staff were messageable through the same system.
Meaning the attack path became:
attacker sends malicious message
↓
staff account receives message
↓
chat auto-renders content
↓
payload executes in privileged session
No phishing.
No tricking an employee into clicking a link.
No social engineering.
No fake support request.
Just:
send message → wait
That dramatically changes impact.
Because the question becomes:
What can a privileged session do?
And on gaming/media platforms, staff panels often expose:
moderation tools
account management
content controls
support actions
internal APIs
elevated account visibility
Even if there is no formal admin panel access, staff accounts frequently have elevated trust boundaries.
Which means a normal stored XSS becomes:
privileged-user compromise
At that point, realistic impact starts looking more like:
session theft
forced authenticated actions
internal API interaction
privilege escalation paths
moderator account compromise
The exploitability problem had already been solved by the zero-click behavior.
The staff angle only made the blast radius bigger.
Demonstrating Impact
One thing bounty programs frequently want for XSS reports is proof that:
JavaScript execution actually occurred in a meaningful context
Not just:
alert(1)
During validation, I demonstrated execution in the authenticated browser context.
Example proof-of-execution:
document.cookie
Observed output:
access_token=eyJhbGc...redacted
session=sess_91fa4...
csrf=2f0c81...
This proved two things:
JavaScript executed inside an authenticated session
Sensitive browser context was accessible
That mattered during triage because the report initially lacked enough reproduction detail.
Which leads to the annoying part of the story.
Triage Friction: “Needs More Info”
The original submission was straightforward.
Stored XSS.
Payload.
Impact.
Basic screenshot.
In my opinion:
enough to understand the bug.
Triage disagreed.
The report moved to:
Needs More Info
Requested details included:
vulnerable parameter
reproduction steps
payloads used
impact explanation
proof of token access
screenshot showing authenticated context
This is one of those moments most hunters eventually experience.
You submit what feels like an obvious bug.
Triage asks for:
everything short of a live demo
In hindsight:
They were not entirely wrong.
The original report was technically correct, but under-documented.
And bounty triage teams are reading hundreds of reports.
The easier you make confirmation:
the faster you get paid.
I refined the report into something closer to:
Description:
Stored XSS in chat body
Location:
Chat message rendering
Steps:
1. Login
2. Navigate to /talk
3. Start or continue conversation
4. Send crafted payload
5. Payload executes automatically upon render
Impact:
Stored zero-click JavaScript execution against any reachable user, including staff accounts.
I also included authenticated-context screenshots proving execution.
After refinement:
Status: Approved
A few months later:
Status: Confirmed
Eventually:
Status: Resolved
What Made This Bug Interesting
Individually, none of these traits are unusual.
Stored XSS?
Common.
Chat rendering bugs?
Common.
Staff reachability?
Not rare.
The interesting part was the combination:
stored
+
zero-click
+
real-time rendering
+
staff reachable
=
high-confidence exploitation
That combination dramatically reduced friction.
And reduced friction is what makes attacks realistic.
A lot of XSS bugs sound scary on paper but are annoying to weaponize.
This one was the opposite.
Minimal effort.
Normal feature abuse.
High likelihood of execution.
Hunter Takeaways
1. Chat Systems Deserve More Attention
Especially when platforms support:
live updates
websocket rendering
rich formatting
embedded content
Chat UIs frequently become DOM nightmares.
2. Partial Filtering Usually Means “Keep Going”
If basic payloads fail:
Do not stop.
Try understanding:
what the renderer actually trusts
Blacklist-based filtering breaks constantly.
3. Zero-Click Changes Everything
Always ask:
What user interaction is required?
Because:
stored XSS + click
is very different from:
stored XSS + no interaction
Exploitability matters.
A lot.
4. Reachability Impacts Severity
Ask:
Who can receive this payload?
If:
normal users only
impact may stay moderate.
If:
staff
moderators
support
admins
the severity conversation changes very quickly.
Final Thoughts
This bug taught me something simple:
Good XSS is usually about delivery, not payloads.
The payload itself was not particularly clever.
The interesting part was:
automatic rendering
zero-click execution
persistent storage
privileged target reachability
If I had stopped after seeing filters block obvious payloads, I probably would have missed it.
Instead, a few more minutes of testing turned:
“maybe HTML injection”
into
“stored zero-click XSS against reachable staff accounts.”





