Feb 16, 2026·6 min read·12 visits
A Reflected XSS vulnerability in SiYuan's `/api/icon/getDynamicIcon` endpoint allows attackers to inject arbitrary JavaScript via the `content` parameter. The server reflects this input into an unsanitized SVG response served with an XML MIME type, leading to immediate code execution in the victim's browser.
In the world of Personal Knowledge Management (PKM), we treat our digital notebooks as extensions of our brains—secure, private, and trusted. But what happens when your 'second brain' has a hole in it? CVE-2026-23847 exposes a Reflected Cross-Site Scripting (XSS) vulnerability in SiYuan, a popular open-source privacy-first note-taking app. By abusing the dynamic icon generation API, attackers can inject malicious JavaScript payload directly into SVG images rendered by the browser. This isn't just about popping alert boxes; it's about compromising the integrity of a user's entire knowledge base through a single, carelessly crafted link.
SiYuan pitches itself as a privacy-first, local-first personal knowledge management system. It's the kind of tool where you store your deepest thoughts, your startup ideas, and maybe even your passwords (though please, don't do that). The architecture is interesting—it's often self-hosted or run locally, serving a web interface to the user. This makes it a web app, subject to all the classic web vulnerabilities, even if it feels like a desktop application.
Deep within the application's API lies a feature designed to make your notes look pretty: getDynamicIcon. Its job is simple. You give it some text, and it generates a sleek SVG icon on the fly to display next to your document titles. It's a nice touch for UI polish.
But here's the thing about 'simple' features: they are often where developers let their guard down. The assumption is, "It's just generating an icon, what could go wrong?" As it turns out, when you mix user input with vector graphics and forget to sanitize, the answer is "everything."
The vulnerability lives in kernel/api/icon.go. The logic is painfully straightforward. The application accepts a content query parameter, representing the text you want inside your icon. It then constructs an SVG string by concatenating this input into a template. Specifically, when the type parameter is set to 8 (text-based icons), the code grabs the string and drops it right into a <text> element.
Here is the fatal mistake: The application serves this response with the Content-Type: image/svg+xml. To a modern browser, an SVG served this way is not just a picture; it's an XML document capable of executing JavaScript. If the application had served it as image/png or sanitized the input, we wouldn't be having this conversation.
Instead, the developers performed direct string interpolation without escaping XML special characters. This is the equivalent of locking your front door but leaving the window wide open with a neon sign pointing to it. An attacker doesn't need to be a wizard; they just need to know how to close a tag.
Let's look at the crime scene. In the vulnerable version of kernel/api/icon.go, the code effectively did this:
// The Vulnerable Logic
func getDynamicIcon(c *gin.Context) {
content := c.Query("content")
// ... font calculation logic ...
// DIRECT INJECTION
svg := fmt.Sprintf(`<svg ...><text ...>%s</text></svg>`, content)
c.Header("Content-Type", "image/svg+xml")
c.String(http.StatusOK, svg)
}See that %s? That's where the dragon lives. There is zero validation. If content contains </text><script>..., Go happily prints it into the buffer.
The fix, introduced in commit 5c0cc375b47567e15edd2119066b09bb0aa18777, adds a sanity check. They didn't just escape the output; they added a scrubber function and a configuration toggle.
// The Patched Logic
func getDynamicIcon(c *gin.Context) {
// ... generation logic ...
// The Guard Rail
if !model.Conf.Editor.AllowSVGScript {
svg = util.RemoveScriptsInSVG(svg)
}
c.Header("Content-Type", "image/svg+xml")
c.String(http.StatusOK, svg)
}The util.RemoveScriptsInSVG function is now the bouncer at the club, kicking out any <script> tags before they hit the browser. It's a blacklist approach, which makes security researchers twitchy (whitelists are always better), but for this specific vector, it stops the bleeding.
Exploiting this is trivial. We need to construct a URL that, when clicked by a victim, renders an SVG containing our malicious payload. Since the input is placed inside a <text> tag, we first need to break out of that context.
The Payload Construction:
</text><script>alert(document.domain)</script><text> (to keep the SVG valid enough to render, purely optional for execution but good for stealth).The Final URL:
http://target-siyuan-instance/api/icon/getDynamicIcon
?type=8
&content=</text><script>alert('Pwned')</script><text>When the victim clicks this link, their browser requests the icon. The server reflects the payload back with Content-Type: image/svg+xml. The browser sees the <script> tag and executes it in the origin of the SiYuan instance.
> [!WARNING]
> Re-exploitation Potential: The patch uses a function called RemoveScriptsInSVG. If this function creates a blacklist (e.g., regex replacing <script>), it might be bypassable using alternative SVG execution vectors like <a xlink:href="javascript:..."> or event handlers like <svg onload=...>. A determined attacker would immediately fuzz this sanitizer against the SVG specification.
This is a Reflected XSS, meaning it requires user interaction (clicking a link). However, in the context of a personal knowledge management system, the impact is severe.
If an attacker tricks a user into clicking a malicious link while they are logged into their SiYuan instance:
Essentially, if you click the wrong link, your private "second brain" becomes public property.
The immediate fix is to upgrade SiYuan to version 3.5.4 or later. The developers have patched the specific hole by stripping scripts from generated SVGs.
Beyond the patch, there is a configuration setting you should verify. In Settings -> Editor, ensure that "Allow execution of SVG scripts" is disabled. This is the default in newer versions, but if you've been tinkering with configs, check it.
For developers reading this: Never use string concatenation to build XML or HTML. Use proper encoding libraries. And if you must serve user-generated SVGs, serve them with Content-Security-Policy: script-src 'none' or force them to download via Content-Disposition: attachment to prevent inline execution.
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 b3log | < 3.5.4 | 3.5.4 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network (Reflected) |
| CVSS v3.1 | 6.1 (Medium) |
| EPSS Score | 0.00028 (Top 8%) |
| Impact | Confidentiality & Integrity Loss |
| Exploit Status | PoC Available |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')