GHSA-pmc9-f5qr-2pcr SiYuan has a SVG Sanitizer Bypass via Whitespace in `javascript:` URI — Unauthenticated XSS
SVG Sanitizer Bypass via Whitespace in `javascript:` URI — Unauthenticated XSSSummarySiYuan'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](https://github.com/siyuan-note/siyuan/security/advisories/GHSA-5hc8-qmg8-pw27).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.9Root CauseThe 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:`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](https://url.spec.whatwg.org/#url-parsing), 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 ConceptVector 1: Tab character (`&#9;`)Vector 2: Newline character (`&#10;`)Vector 3: Carriage return (`&#13;`)Vector 4: Multiple whitespace charactersProcessing trace**Input:** `<a href="java&#9;script:alert(document.domain)">`**html.Parse:** Decodes entity → attribute value = `java\tscript:alert(document.domain)`**Sanitizer:** `TrimSpace(ToLower(val))` = `java\tscript:alert(document.domain)` (tab preserved in middle)**HasPrefix check:** `"java\tscript:..."` does NOT start with `"javascript:"` → **passes through****html.Render:** Outputs literal tab character in href (tabs are not HTML-special)**Browser URL parser:** Strips tab per WHATWG URL spec → `javascript:alert(document.domain)`**User clicks link → JavaScript executes**Attack ScenarioSame as CVE-2026-29183 / advisory #01:Attacker crafts a malicious `getDynamicIcon` URLVictim navigates to the URL (or is redirected)SVG renders with `Content-Type: image/svg+xml`Victim clicks the text link in the SVGJavaScript executes in SiYuan's originAttacker steals session cookies, API tokens, or makes authenticated API callsImpact**Severity:** CRITICAL (CVSS ~9.1)**Type:** CWE-79 (Improper Neutralization of Input During Web Page Generation)Unauthenticated reflected XSS via SVG injectionExecutes in the SiYuan application originBypasses the fix for CVE-2026-29183Independent of the `<animate>` element bypass (advisory #01) — different root causeSuggested FixReplace the simple `HasPrefix` check with whitespace-stripped comparison:This should also be applied to the `data:` URI check, as the same whitespace bypass could potentially affect it.
CVSS v4.0 score: 6.4