CVE-2026-59832High· 7.7▾ TwilightSiyuan: Authenticated path traversal in /snippets/ static handler (serveSnippets) leaks conf/conf.json secrets and siyuan.db
▾ Twilight zone — High severity, or a signal on a lesser flaw
impact 42.4 · likelihood 0.1 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
Exploit-prediction probability, daily snapshots since Sep 2.
Disclosure to exploitation, from the record and what we observed since indexing it.
Disclosed via OSV
0.5%
Last analysed / modified upstream
Reporter: Cavan Loughran, Celvex Group Inc.
The /snippets/*filepath route handler serveSnippets in kernel/server/serve.go performs a bare filepath.Join(util.SnippetsPath, filePath) on the single-decoded c.Request.URL.Path and serves the result with c.File(), with NO IsSubPath containment and NO IsSensitivePath denylist - unlike the sibling /export/ (serveExport) and /appearance/ (serveAppearance) handlers, which both carry IsSubPath, and unlike /assets/ (serveAssets), whose traversal was fixed in GHSA-p4m3-mgmm-c664. Because util.SnippetsPath = WorkspaceDir/data/snippets, an authenticated request to GET /snippets/%2e%2e/%2e%2e/conf/conf.json resolves to WorkspaceDir/conf/conf.json and leaks the kernel API token and AccessAuthCode (the same secret file CVE-2026-30869 leaked from /export/); GET /snippets/%2e%2e/%2e%2e/temp/siyuan.db leaks the full document database.
v3.6.5 and current master (verified by direct source read). The /export/ and /assets/ fixes were endpoint-scoped and never reached serveSnippets.
Sink, kernel/server/serve.go, serveSnippets (verbatim, current master and v3.6.5):
func serveSnippets(ginServer *gin.Engine) { ginServer.Handle("GET", "/snippets/*filepath", model.CheckAuth, func(c *gin.Context) { filePath := strings.TrimPrefix(c.Request.URL.Path, "/snippets/") if !model.IsAdminRoleContext(c) { if "conf.json" == filePath { c.Status(http.StatusUnauthorized) return } } ext := filepath.Ext(filePath) name := strings.TrimSuffix(filePath, ext) confSnippets, err := model.LoadSnippets() ... for _, s := range confSnippets { if s.Name == name && ("" != ext && s.Type == ext[1:]) { c.Header("Content-Type", mime.TypeByExtension(ext)) c.String(http.StatusOK, s.Content) return } } // when not matched in the config file, look it up on the filesystem filePath = filepath.Join(util.SnippetsPath, filePath) // <-- TAINTED join, no containment c.File(filePath) // <-- arbitrary workspace file read }) }
Taint path, end to end:
Directory layout (confirmed by kernel/util/working.go): WorkspaceDir/ data/snippets/ = util.SnippetsPath (the /snippets/ base) conf/conf.json <-- API token + AccessAuthCode (the secret) temp/siyuan.db <-- full SQLite database From util.SnippetsPath = WorkspaceDir/data/snippets the climb-out is exactly two levels:
SiYuan has been fixing path traversal in file-serving handlers ONE endpoint at a time:
The route is gated by model.CheckAuth only (any authenticated user), NOT CheckAdminRole. CheckAuth admits any principal that presents a valid API token (Conf.Api.Token), a valid session whose AccessAuthCode == Conf.AccessAuthCode, or BasicAuth. The handler's own if !model.IsAdminRoleContext(c) branch confirms non-admin reachability; that branch only blocks the literal string "conf.json", which the traversal payload "../../conf/conf.json" does not match, so even non-admins leak the secret file. SiYuan supports non-admin authenticated roles (RoleEditor, RoleReader) in shared/published workspace modes, plus access-auth-code logins. Privilege required: PR:L (a valid authenticated session), NOT pre-auth and NOT admin-gated. Reading conf/conf.json yields the admin API token/AccessAuthCode, letting a non-admin escalate to full kernel-admin API control; siyuan.db leaks all note content. The kernel HTTP server is the published interface for self-hosted/Docker deployments (AV:N).
SiYuan's SECURITY.md excludes arbitrary file WRITE outside the workspace as a non-issue, but this finding is arbitrary file READ of in-workspace secrets (conf/conf.json, siyuan.db) and host files. Read-side traversal of conf.json is exactly what CVE-2026-30869 was accepted for, so this is squarely in scope.
Non-destructive: no weaponized exploit is included; the chain is described in prose for the maintainer to reproduce.
Secondary (reported for completeness, not the headline): serveRepoDiff (/repo/diff/*path) shares the same bare filepath.Join(util.TempDir, "repo", "diff", requestPath) + http.ServeFile with NO containment, so .. in requestPath escapes TempDir/repo/diff. BUT it carries model.CheckAdminRole (admin-only), so the trust boundary crossed is weak (an admin already holds the API token). Lower severity; shares the same one-line fix.
Authenticated (non-admin, PR:L) arbitrary workspace file read: kernel API token + AccessAuthCode (conf/conf.json), the full document database (siyuan.db), and host files outside the workspace. Leaking conf.json enables escalation to full kernel-admin API control (and, per the parent CVE, can be chained toward RCE).
GitHub Security Advisories for siyuan-note/siyuan include GHSA-2h2p-mvfx-868w / CVE-2026-30869 (/export/), GHSA-hjh7-r5w8-5872 / CVE-2026-41894 (/export/ double-encode), GHSA-p4m3-mgmm-c664 (/assets/ double-encode), plus stored-XSS/template-injection advisories. NONE references /snippets/ or serveSnippets. OSV / GitLab Advisory Database for the Go module github.com/siyuan-note/siyuan/kernel lists only the /export/ and /assets/ path-traversal entries. Not a duplicate; the contribution is the distinct, unpatched sibling handler serveSnippets reached at a non-admin authenticated privilege.
Add the same containment SiYuan already uses in serveExport/serveAppearance: resolve filePath and reject if !gulu.File.IsSubPath(util.SnippetsPath, resolved), and apply util.IsSensitivePath. The identical one-line containment also closes the admin-only /repo/diff/*path (serveRepoDiff) handler.
CWE: CWE-22 (Path Traversal), related CWE-23 (Relative Path Traversal). CVSS v3.1 7.7 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N).
Coordinated-disclosure terms: 90 days from acknowledgement before public disclosure, aligned earlier if a fix ships sooner. No public issue / PR / gist / post has been or will be opened before a coordinated date or a shipped fix. No weaponized PoC has been shared.
github.com/siyuan-note/siyuan/kernel < 0.0.0-20260704035520-68cc0f537dfaUpgrade to a patched release:
github.com/siyuan-note/siyuan/kernel 0.0.0-20260704035520-68cc0f537dfaConnected by shared product, vendor, weakness, or advisory.
CVE-2026-69086High· 7.7SiYuan: Path Traversal via unvalidated avID in RenderAttributeView/AV read endpoints : reader-reachable cross-scope attribute-view disclo…
CVE-2026-54066High· 7.5SiYuan: Path Traversal via Double URL Encoding in /assets/*path (publish mode arbitrary file─read), Incomplete fix of CVE-2026-41894
GHSA-57v5-wqx3-cgj4Medium· 5.8SiYuan: Database view structure (all view names, layout types and per-field visibility) is returned to anonymous readers by /api/av/getAt…
CVE-2026-72790Medium· 5.8SiYuan: Notebook name, document count, size and timestamps are returned for any notebook, including notebooks hidden from readers, by /ap…
CVE-2026-72799Medium· 5.8SiYuan: Missing publish-access filter on the HPath/path-resolution endpoints discloses the private document tree to anonymous readers
CVE-2026-72794High· 8.6SiYuan: The session-cookie signing key (Conf.CookieKey) is returned to anonymous readers by /api/system/getConf