CVE-2026-31809

CVE-2026-31809 is a medium-severity cross-site scripting (XSS) vulnerability in github.com/siyuan-note/siyuan/kernel (go), affecting versions < 0.0.0-20260310025236-297bd526708f. It is fixed in 0.0.0-20260310025236-297bd526708f.

Summary

SVG Sanitizer Bypass via Whitespace in javascript: URI, Unauthenticated XSS

SiYuan's SVG sanitizer (SanitizeSVG) checks href attributes for the javascript: prefix using strings.HasPrefix(). However, inserting ASCII tab (&#9;), newline (&#10;), or carriage return (&#13;) characters inside the javascript: string bypasses this prefix check. Browsers strip these characters per the WHATWG URL specification before parsing the URL scheme, so the JavaScript still executes. This allows an attacker to inject executable JavaScript into the unauthenticated /api/icon/getDynamicIcon endpoint, creating a reflected XSS.

This is a second bypass of the fix for CVE-2026-29183 (fixed in v3.5.9), distinct from the <animate> element bypass.

Affected Component

  • File: kernel/util/misc.go
  • Function: SanitizeSVG() (lines 234-319)
  • Specific check: Line 271, strings.HasPrefix(val, "javascript:")
  • Endpoint: GET /api/icon/getDynamicIcon?type=8&content=... (unauthenticated)
  • Version: SiYuan <= 3.5.9

Root Cause

The sanitizer uses Go's html.Parse which decodes HTML entities in attribute values. When the input contains java&#9;script:alert(1), the parser decodes &#9; to a literal tab character (U+0009). The sanitizer then checks:

val := strings.TrimSpace(strings.ToLower(a.Val))
// val is now "java\tscript:alert(1)"

if strings.HasPrefix(val, "javascript:") {
    continue  // This check FAILS, tab breaks the prefix match
}

strings.TrimSpace only removes leading/trailing whitespace, not internal whitespace. The HasPrefix check fails because "java\tscript:..." does not start with "javascript:".

However, per the WHATWG URL Standard, step 1 of URL parsing removes all ASCII tab and newline characters (U+0009, U+000A, U+000D) from the input. So the browser parses java\tscript:alert(1) as javascript:alert(1).

Proof of Concept

Vector 1: Tab character (&#9;)

GET /api/icon/getDynamicIcon?type=8&content=</text><a href="java&#9;script:alert(document.domain)"><text x="50%25" y="80%25" fill="red" style="font-size:60px">Click me</text></a><text>&color=blue

Vector 2: Newline character (&#10;)

GET /api/icon/getDynamicIcon?type=8&content=</text><a href="java&#10;script:alert(document.domain)"><text x="50%25" y="80%25" fill="red" style="font-size:60px">Click me</text></a><text>&color=blue

Vector 3: Carriage return (&#13;)

GET /api/icon/getDynamicIcon?type=8&content=</text><a href="java&#13;script:alert(document.domain)"><text x="50%25" y="80%25" fill="red" style="font-size:60px">Click me</text></a><text>&color=blue

Vector 4: Multiple whitespace characters

GET /api/icon/getDynamicIcon?type=8&content=</text><a href="j&#9;a&#10;v&#13;a&#9;s&#10;c&#13;r&#9;i&#10;p&#13;t:alert(document.domain)"><text x="50%25" y="80%25" fill="red" style="font-size:60px">Click me</text></a><text>&color=blue

Processing trace

  1. Input: <a href="java&#9;script:alert(document.domain)">
  2. html.Parse: Decodes entity → attribute value = java\tscript:alert(document.domain)
  3. Sanitizer: TrimSpace(ToLower(val)) = java\tscript:alert(document.domain) (tab preserved in middle)
  4. HasPrefix check: "java\tscript:..." does NOT start with "javascript:"passes through
  5. html.Render: Outputs literal tab character in href (tabs are not HTML-special)
  6. Browser URL parser: Strips tab per WHATWG URL spec → javascript:alert(document.domain)
  7. User clicks link → JavaScript executes

Attack Scenario

Same as CVE-2026-29183 / advisory #01:

  1. Attacker crafts a malicious getDynamicIcon URL
  2. Victim navigates to the URL (or is redirected)
  3. SVG renders with Content-Type: image/svg+xml
  4. Victim clicks the text link in the SVG
  5. JavaScript executes in SiYuan's origin
  6. Attacker steals session cookies, API tokens, or makes authenticated API calls

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
  • Bypasses the fix for CVE-2026-29183
  • Independent of the <animate> element bypass (advisory #01), different root cause

Untrusted input is rendered as active markup in a victim's browser, which can run script in their session. Typical impact: session or credential theft, and actions taken as the user.

Affected versions

github.com/siyuan-note/siyuan/kernel (< 0.0.0-20260310025236-297bd526708f)

Security releases

github.com/siyuan-note/siyuan/kernel → 0.0.0-20260310025236-297bd526708f (go)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

See it in your environment

Remediation advice

Replace the simple HasPrefix check with whitespace-stripped comparison:

// Strip ASCII tab, newline, CR before checking for javascript: prefix
cleaned := strings.Map(func(r rune) rune {
    if r == '\t' || r == '\n' || r == '\r' {
        return -1  // Remove character
    }
    return r
}, val)

if key == "href" || key == "xlink:href" || key == "xlinkhref" {
    if strings.HasPrefix(cleaned, "javascript:") {
        continue
    }
    if strings.HasPrefix(cleaned, "data:") {
        if strings.Contains(cleaned, "text/html") || strings.Contains(cleaned, "image/svg+xml") || strings.Contains(cleaned, "application/xhtml+xml") {
            continue
        }
    }
}

This should also be applied to the data: URI check, as the same whitespace bypass could potentially affect it.

Frequently Asked Questions

  1. What is CVE-2026-31809? CVE-2026-31809 is a medium-severity cross-site scripting (XSS) vulnerability in github.com/siyuan-note/siyuan/kernel (go), affecting versions < 0.0.0-20260310025236-297bd526708f. It is fixed in 0.0.0-20260310025236-297bd526708f. Untrusted input is rendered as active markup in a victim's browser, which can run script in their session.
  2. Which versions of github.com/siyuan-note/siyuan/kernel are affected by CVE-2026-31809? github.com/siyuan-note/siyuan/kernel (go) versions < 0.0.0-20260310025236-297bd526708f is affected.
  3. Is there a fix for CVE-2026-31809? Yes. CVE-2026-31809 is fixed in 0.0.0-20260310025236-297bd526708f. Upgrade to this version or later.
  4. Is CVE-2026-31809 exploitable, and should I be worried? Whether CVE-2026-31809 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  5. What actually determines whether CVE-2026-31809 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  6. How do I fix CVE-2026-31809? Upgrade github.com/siyuan-note/siyuan/kernel to 0.0.0-20260310025236-297bd526708f or later.

Other vulnerabilities in github.com/siyuan-note/siyuan/kernel

CVE-2026-45375CVE-2026-45371CVE-2026-45148CVE-2026-45147CVE-2026-44588

Stop the waste.
Protect your environment with Kodem.