Feb 17, 2026·5 min read·3 visits
SiYuan Note versions prior to 3.5.4-dev2 fail to sanitize uploaded SVG files. Attackers can embed JavaScript in these 'images', leading to Stored XSS. The vendor patch attempts to strip <script> tags but misses event handlers, allowing for immediate re-exploitation.
A critical Stored Cross-Site Scripting (XSS) vulnerability in SiYuan Note allows attackers to weaponize the 'local-first' knowledge base. By uploading malicious SVG assets, an attacker can execute arbitrary JavaScript within the context of the application. The vendor's initial attempt to patch this issue relies on incomplete blacklisting, leaving the door open for trivial bypasses.
SiYuan Note pitches itself as a privacy-focused, local-first knowledge management system. It's where users dump their most sensitive thoughts, passwords, and research, trusting that "local-first" means "secure from the web." It supports Markdown, block-based editing, and rich assets.
That last part—rich assets—is where things get messy. To make notes look pretty, SiYuan allows users to upload images. Among the supported formats is SVG (Scalable Vector Graphics). If you've been in the security game for more than ten minutes, you know exactly where this is going.
SVGs aren't just pictures; they are XML documents. They support the full power of the DOM, including JavaScript execution. When an application treats an SVG like a harmless PNG, it's not just displaying an image; it's rendering an active document. If you control the document, you control the viewer.
The root cause here is a classic "file type confusion" in security architecture. The developers implemented a feature to upload assets but failed to scrub them for active content. In versions prior to 3.5.4-dev2, the application accepted any file with an .svg extension and served it back to the browser with Content-Type: image/svg+xml.
Browsers are obedient. When they see image/svg+xml, they parse the XML. If that XML contains a <script> tag, the browser executes it. Because SiYuan serves these assets from the same origin as the application, that script runs with full trust.
This is Stored XSS 101. An attacker uploads malware.svg as an attachment to a shared note or a public workspace. When the victim opens the note—or even just loads the asset URL—the payload fires. No clicks required, just rendering the "image."
The vendor attempted to fix this in version 3.5.4-dev2 (commit 11115da3). They introduced a toggle allowSVGScript (default false) and a sanitization function RemoveScriptsInSVG. Let's look at the logic they added to kernel/server/serve.go and the utility package.
The logic is essentially a blacklist filter:
// Pseudo-code of the fix implementation
func RemoveScriptsInSVG(data []byte) []byte {
doc, _ := html.Parse(bytes.NewReader(data))
// Recursively walk the DOM
var f func(*html.Node)
f = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "script" {
// Remove the node
return
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
f(doc)
// Re-render HTML
}Do you see the problem? They are explicitly hunting for the <script> tag. That's it. They completely ignored the dozens of other ways to execute JavaScript in an SVG.
This is the coding equivalent of locking your front door but leaving the garage open, the windows smashed, and a "Free TV" sign on the lawn. By filtering only the script tag, they stop the most basic script kiddie, but anyone who has read the MDN documentation for SVG knows what comes next.
First, here is the standard exploit that works against unpatched versions (< 3.5.4-dev2). It's a standard XML file with a script block:
<!-- CVE-2026-23645 Standard Payload -->
<svg xmlns="http://www.w3.org/2000/svg">
<script>alert('Pwned via Script Tag')</script>
</svg>Now, let's look at the bypass for the patched version. Since the code only removes <script> nodes, we can use event handlers. The onload event fires immediately when the SVG is rendered. The sanitization function leaves attributes completely untouched.
<!-- Patch Bypass Payload -->
<svg xmlns="http://www.w3.org/2000/svg" onload="alert('Pwned via Event Handler')">
<rect width="100" height="100" fill="red" />
</svg>Or, if we want to be fancy and use links:
<svg xmlns="http://www.w3.org/2000/svg">
<a href="javascript:alert(1)">
<text x="20" y="20">Click me for a prize</text>
</a>
</svg>If the application is running in an Electron wrapper (which SiYuan often does), and node integration is enabled or context isolation is weak, this XSS can escalate to Remote Code Execution (RCE) by accessing Node.js primitives via the window object.
The current mitigation provided by the vendor is a configuration flag: allowSVGScript. Ensure this is set to false, but understand that strictly speaking, the software is still vulnerable to the bypasses described above if the sanitizer isn't improved.
For the developers, the lesson is simple: Don't write your own sanitizers. You will fail. HTML and XML are too complex, and browsers are too forgiving.
The correct fix involves using a battle-tested library like DOMPurify (frontend) or a strict server-side policy that whitelists specific tags (like rect, path, circle) and attributes (fill, stroke, width), while aggressively stripping everything else. If you are stripping bad things (blacklisting), you have already lost. You must only allow known good things (whitelisting).
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
SiYuan Note SiYuan | < 3.5.4-dev2 | 3.5.4-dev2 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-23645 |
| CVSS v3.1 | 6.1 (Medium) |
| CWE | CWE-79 (Improper Neutralization of Input) |
| Attack Vector | Network (Stored XSS) |
| Impact | Session Hijacking / Potential RCE |
| Fix Status | Patched (Incomplete Sanitization) |
The software does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.