# 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](https://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:

```text
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:

```html
<b>hello</b>
```

Then basic render probes:

```html
<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:

```html
<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:

```http
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:

```json
{
  "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:

```text
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:

```html
<script>alert(1)</script>
```

```html
<img src=x onerror=alert(1)>
```

```html
<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:

```html
<IFRAME SRC="javascript:window['al'+'ert']('[redacted]')"></IFRAME>
```

The payload survived filtering because:

*   the renderer trusted iframe tags
    
*   the `javascript:` context was not blocked
    
*   simple 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:

```text
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:

```text
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:

```javascript
alert(1)
```

During validation, I demonstrated execution in the authenticated browser context.

Example proof-of-execution:

```javascript
document.cookie
```

Observed output:

```text
access_token=eyJhbGc...redacted
session=sess_91fa4...
csrf=2f0c81...
```

This proved two things:

1.  JavaScript executed inside an authenticated session
    
2.  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:

```text
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:

```text
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:

```text
Status: Approved
```

A few months later:

```text
Status: Confirmed
```

Eventually:

```text
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:

```text
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:

```text
stored XSS + click
```

is very different from:

```text
stored XSS + no interaction
```

Exploitability matters.

A lot.

* * *

**4\. Reachability Impacts Severity**

Ask:

> Who can receive this payload?

If:

```text
normal users only
```

impact may stay moderate.

If:

```text
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.”
