Roundcube round two: three more sanitizer bypasses Three more bypasses in Roundcube's HTML sanitizer: SMIL animation attributes load remote resources, unquoted body backgrounds enable CSS injection, and position:fixed !important enables phishing overlays. Date: 2026-03-18 Last Modified: 2026-03-18 Tags: vulnerability, roundcube, svg, css, email-security URL: https://nullcathedral.com/posts/2026-03-18-roundcube-round-two-three-more-sanitizer-bypasses/ ------------------------------------------------------------------------ TL;DR: Three more bypasses in Roundcube's HTML sanitizer. SMIL animation values and by attributes pass through without URI validation. The body background attribute gets dropped into url() unquoted, which allows CSS injection. And position: fixed !important bypasses the fixed-position mitigation for full-viewport phishing overlays. Fixed in 1.5.14, 1.6.14, and 1.7-rc5. Vulnerability information Field | Value Vendor | Roundcube Product | Roundcube Webmail Affected versions | < 1.5.14, 1.6.x < 1.6.14, 1.7-rc1 through 1.7-rc4 CVE | Pending Disclosure date | 2026-03-18 Going back After the feImage bypass was patched, the sanitizer still felt fairly fragile. Same patterns everywhere: hand-maintained allowlists, attribute routing that assumed it caught everything. I was bored and didn't feel like doing anything else, so I went poking at it again.[1] Three more things came out of it. SMIL animations: values and by The feImage bug was about an element whose href went through the wrong code path. This one is about attributes that skip the code path entirely. SMIL animation elements (, , ) can target any attribute on their parent element. The to and from attributes set start and end values, but SMIL also has values (semicolon-separated keyframes) and by (relative offset). They all do the same thing: set the value of the target attribute. wash_attribs() handles to and from correctly by resolving attributeName and routing the value through the appropriate URI check: rcube_washtml.php // in SVG to/from attribs may contain anything, including URIs if ($key == 'to' || $key == 'from') { $key = strtolower((string) $node->getAttribute('attributeName')); $key = trim(preg_replace('/^.*:/', '', $key)); if ($key && !isset($this->_html_attribs[$key])) { $key = null; } } This swaps the attribute name for the resolved attributeName, so to="https://httpbin.org" on gets validated as if it were an href value. The right idea. But values and by aren't in that if check. They're both in the $html_attribs allowlist, so they pass the initial gate and fall through to the generic pass-through: rcube_washtml.php } elseif ($key) { $out = $value; } No URI validation. The value goes into the output unchanged. There's a second layer to this. After CVE-2024-37383[2], the sanitizer blocks SMIL animations targeting attributeName="href": rcube_washtml.php } elseif (in_array($tagName, ['animate', 'animatecolor', 'set', 'animatetransform']) && self::attribute_value($node, 'attributename', 'href') ) { Only href. But CSS properties like mask and cursor also load external resources when their values contain URLs. An passes through because the element-level block doesn't fire. Combined: the values attribute bypasses attribute-level validation, and targeting mask instead of href bypasses element-level blocking. Both checks miss. Proof of concept Zero-click open tracking. An invisible SVG off-screen: Open the email in Roundcube with "Block remote images" enabled. The browser fires a GET to the attacker's URL.[3] SMIL's keyTimes attribute enables timed beacons, measuring how long a recipient views the email: Body backgrounds: unquoted url() This one is in the application layer, not the sanitizer library. When Roundcube renders an email, washtml_callback() processes the element's attributes and converts them to inline CSS on the output container
. The background attribute[4] becomes background-image: index.php case 'background': if (preg_match('/^([^\s]+)$/', $value, $m)) { $style['background-image'] = "url({$value})"; } break; The value runs through wash_uri() before this point, which allows data:image/* URIs. Then it gets interpolated into url() without quotes. A ) inside the data URI terminates the url() function early, and everything after it is parsed as additional CSS properties on the container
. Because the injected CSS is inline style (not inside a

Session Expired

Your Roundcube session has expired due to inactivity.

Sign In Again
The overlay covers the preview pane iframe. When the message is opened in a new window (no iframe), it covers the full browser viewport with no visible Roundcube chrome. A "session expired" dialog pointing to an attacker-controlled login page. Impact The SMIL and body background bypasses defeat "Block remote images" to enable email tracking: open confirmation, IP logging, browser fingerprinting. The SMIL variant can also measure read time through timed beacon sequences. The position: fixed bypass is a phishing vector. A full-viewport overlay can impersonate Roundcube's own login prompt. Remediation All three issues are fixed in 1.5.14, 1.6.14, and 1.7-rc5. See the Roundcube advisory. SMIL animations The fix (82ab5ec) replaces the inline element-level check with a new is_insecure_tag() method that blocks SMIL animation elements targeting href, or mask/cursor when the values attribute contains url(): rcube_washtml.php private static function is_insecure_tag($node) { $tagName = strtolower($node->nodeName); if (!in_array($tagName, ['animate', 'animatecolor', 'set', 'animatetransform'])) { return false; } if (self::attribute_value($node, 'attributeName', '/^href$/i')) { return true; } return self::attribute_value($node, 'attributeName', '/^(mask|cursor)$/i') && self::attribute_value($node, 'values', '/url\(/i'); } The fix blocks the entire element when it detects a dangerous combination rather than adding values and by to the attribute-level validation. The attribute_value() helper was also changed from exact string matching to regex matching to support this. Body backgrounds The fix (fd0e981) adds validation in wash_uri() that rejects data URIs unless they're properly base64-encoded: rcube_washtml.php // At this point we allow only valid base64 images if (stripos($type, 'base64') === false || preg_match('|[^0-9a-z\s/+]|i', $matches[2])) { return ''; } The crafted data:image/png,x) payload fails the first check: the type is png, not png;base64, so stripos returns false and the URI is rejected. The second check guards base64-encoded payloads against smuggled characters like ). The unquoted url() interpolation in washtml_callback() is unchanged, but the injection vector is cut off at the URI validation layer. CSS position The fix (226811a) changes the position: fixed check from exact match to substring match: rcube_utils.php } elseif ($property == 'position' && stripos($value, 'fixed') !== false) { $value = 'absolute'; } stripos catches "fixed !important" because "fixed" is a substring. The value gets replaced with "absolute" before it reaches token validation. Update to 1.5.14, 1.6.14, or 1.7-rc5. Timeline Date | Event 2026-03-07 | Reported to Roundcube 2026-03-09 | Triaged 2026-03-18 | 1.5.14, 1.6.14, and 1.7-rc5 released 2026-03-18 | This post [1] Most of the bugs on this blog started with "I was bored." Bored hackers are a threat model. [2] Exploited in the wild against a CIS government organization in June 2024. On CISA's KEV catalog. Roundcube XSS bugs keep ending up in targeted campaigns. Winter Vivern used a different one as a zero-day against European government entities in 2023. [3] Tested on Firefox. Chrome deprecated SMIL in some contexts, but Firefox still evaluates it fully. The mask property with url() values triggers a resource load during SVG rendering. [4] Deprecated since HTML 4.01, listed as a non-conforming feature in the WHATWG Living Standard. Every browser still renders it, and email HTML is stuck in the 90s, so sanitizers have to handle it. [5] Two extra words. That's all it took. ------------------------------------------------------------------------ Source: NULL CATHEDRAL https://nullcathedral.com/