Stored XSS via KaTeX `\href` in SiYuan math blocks (local-kernel editor path)
What fixes it
No source in this snapshot names a fixed version.
Description
Summary
Stored cross-site scripting in SiYuan Note. Math block content ($$...$$) written by any user with edit permission on a document is rendered by the block's editor path with KaTeX's trust: true and without HTML sanitization. KaTeX's TeX \href (and \url, \includegraphics) rules therefore preserve a javascript: URL in the generated <a href> / <img src> attribute, which the application then assigns via innerHTML. A reader who clicks the rendered link executes attacker script in the application origin.
Details
Root cause is the trust decision for KaTeX combined with a direct innerHTML sink, not a generic KaTeX flaw. KaTeX's \href rule is documented as emitting the URL as given when trust is enabled; the application opts into that mode by default on the local path.
Affected source locations (commit 60a4387):
app/src/protyle/render/mathRender.ts:52-59— interpretation entry:
window.katex.renderToString(Lute.UnEscapeHTMLStr(dataContent), { trust: security.trust, ... })(KaTeX pinned to 0.16.9, mathRender.ts:39, katex.min.js?v=0.16.9)
app/src/protyle/render/mathRenderSecurity.ts:1-4— trust/sanitize decisionapp/src/protyle/render/mathRender.ts:68-72— sink:mathElement.innerHTML = mathHTML
Data flow:
- A collaborator with edit rights stores the following as math block content (
$$...$$), through the editor or through the normal APIPOST /api/block/appendBlock {"dataType":"markdown", ...}:$$\href{javascript:alert('xss')}{clickme}$$ - Lute stores it as a math block whose
data-contentholds the TeX source verbatim. - When the document is opened,
mathRendercallskatex.renderToString(..., { trust: true }). KaTeX's\hrefrule emits<a href="javascript:alert('xss')">clickme</a>. - The HTML is mounted with
innerHTMLinto the math block container. - The victim clicks the rendered link; the
javascript:URL runs attacker script in the application origin (javascript:URLs execute only on an in-document click gesture — no click, no execution).
The rendering rule was additionally reproduced in isolation (KaTeX 0.16.9, trust: true) to confirm step 3; the end-to-end click was executed in a real Chromium against the running application.
Proof of concept
Environment: Ubuntu / Docker (b3log/siyuan:latest, digest sha256:d740a1d3ed6b850de3043caf02d3b41e57e0c79b69a8ce2e511588e5e76968c1) / SiYuan 3.8.5, kernel reachable at http://127.0.0.1:18083.
- Plant the payload as an ordinary document editor would (only normal APIs are used):
NB=$(curl -s --noproxy '*' -X POST http://127.0.0.1:18083/api/notebook/createNotebook \
-H 'Content-Type: application/json' -d '{"name":"xsslab"}' \
| python3 -c "import json,sys;print(json.load(sys.stdin)['data']['notebook']['id'])")
DOC="xss-katex-tok-manual"
KDOC=$(curl -s --noproxy '*' -X POST http://127.0.0.1:18083/api/filetree/createDocWithMd \
-H 'Content-Type: application/json' \
-d "{\"notebook\":\"$NB\",\"path\":\"/$DOC\",\"markdown\":\"kx\"}" \
| python3 -c "import json,sys;print(json.load(sys.stdin)['data'])")
TOKEN='xss_manual_p00'
python3 -c "import json;print(json.dumps({'dataType':'markdown',
'data':\"\$\$\\\\href{javascript:alert('$TOKEN')}{clickme}\$\$\",'parentID':'$KDOC'}))" > /tmp/b.json
curl -s --noproxy '*' -X POST http://127.0.0.1:18083/api/block/appendBlock \
-H 'Content-Type: application/json' --data @/tmp/b.json- Open
http://127.0.0.1:18083/stage/build/desktop/→ notebookxsslab→ document$DOC. - The math block renders a
clickmelink. Clicking it shows a native alert dialog containingxss_manual_p00.
Observed result (real Chromium run, payload rendered by the application, not by a test fixture): KaTeX output <a href="javascript:alert('xss_syK1_p00')">…</a>, click → alert dialog with the per-run token, i.e. the payload of the planted block executes in the application origin.
Other payload variants that survive the same rule with trust: true (all require the same single click): \href with mixed case scheme (JaVaScRiPt:), \url{javascript:…}, \includegraphics{javascript:…} (produces img[src="javascript:…"]).
Not vulnerable / checked and safe on the same workspace: ordinary paragraph <img onerror=…> is stored as escaped text; <script> is escaped by Lute; fenced code blocks render as text nodes; HTML block data-content is entity-encoded and not activated; iframe[src="javascript:…"] is emptied to src="" by Lute.
Impact
Stored, so every reader of the shared document is exposed. The script runs in the application origin, so it can read the page and act with the reader's session in that origin — relevant because the desktop build's UI drives local kernel APIs. Demonstrated impact is limited to proof-of-concept execution (alert dialog); no data exfiltration was performed.
Text from GitHub Advisory Database
Affected versions 1
- siyuan, go>= 2.10.0GitHub