high · 7.7CVE-2026-69086Sep 3, 2026

CVE-2026-69086: SiYuan Attribute-View Path Traversal via Unvalidated avID

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

Four read-only attribute-view API endpoints in SiYuan accept a caller-controlled identifier and use it to build a file path without checking that the resulting path stays inside the attribute-view…

Packagegithub.com/siyuan-note/siyuan/kernel
Ecosystemgo
Affected< 0.0.0-20260720151813-0f5a0e7c67b0
Fixed in0.0.0-20260720151813-0f5a0e7c67b0
CVE-2026-69086: SiYuan Attribute-View Path Traversal via Unvalidated avID

The problem

Four endpoints (/api/av/renderAttributeView, /api/av/getAttributeViewKeysByID, /api/av/getAttributeViewKeys, /api/av/getCurrentAttrViewImages) accept a caller-controlled id or avID and pass it directly to GetAttributeViewDataPath, which calls filepath.Join(DataDir, "storage", "av", avID+".json") with no boundary check.

In model.RenderAttributeView, the only identifier guard (ast.IsNodeIDPattern(avID)) lives inside the not-exist/create branch. When the traversal path resolves to a file that already exists, that branch is skipped entirely and av.ParseAttributeView reads the file unconditionally.

The three getAttributeView* endpoints have no create branch at all, so they never reach the guard under any conditions.

All four endpoints require only CheckAuth, which a publish-mode RoleReader token satisfies. When Publish.Auth.Enable is false, the publish proxy uses an anonymous account, making the surface reachable with zero credentials.

Proof of concept

A working proof-of-concept for CVE-2026-69086 in github.com/siyuan-note/siyuan/kernel, with the exact payload below.

http
POST /api/av/renderAttributeView HTTP/1.1
Host: <siyuan-host>:6806
Content-Type: application/json
Authorization: Token <reader-token>

{
  "id": "../../conf/conf"
}

Go's filepath.Join calls filepath.Clean internally, which collapses ../ segments but does not reject them. So filepath.Join(DataDir, "storage", "av", "../../conf/conf.json") resolves cleanly to DataDir/conf/conf.json with no error.

Because conf/conf.json already exists, the !filelock.IsExist(existPath) condition is false, the entire create branch (and the ast.IsNodeIDPattern check inside it) is skipped, and execution falls straight to av.ParseAttributeView. That function re-derives the same path and calls filelock.ReadFile at the traversed location.

No filepath.Rel or IsSubPath check is present at the sink.

The fix moves ast.IsNodeIDPattern validation to before path construction on all branches, and adds filepath.Rel-based confinement at the sink in GetAttributeViewDataPath so every caller inherits it (CWE-22).

The fix

Upgrade to commit 0f5a0e7c67b0 (pseudo-version 0.0.0-20260720151813-0f5a0e7c67b0) or any tagged SiYuan release that includes it. The patch moves ast.IsNodeIDPattern(avID) to before the GetAttributeViewDataPath call on all code paths, and adds sink-side path confinement so that any derived path failing filepath.Rel(avBaseDir, path) is rejected before the file is read.

Reporter not attributed.

References: [1][2][3][4]

Related research