# SiYuan: Path Traversal via unvalidated avID in RenderAttributeView/AV read endpoints : reader-reachable cross-scope attribute-view disclosure

SiYuan kernel builds AV file paths from unvalidated avID; on the load branch the ID check is skipped, letting publish readers traverse out of storage/av/ and read other .json files.

- Payout: Research
- Severity: high
- Program: SiYuan
- Found by: Shirshakhtml
- Disclosed: 2026-09-03
- CVE: CVE-2026-69086
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-7hm9-v7vf-7g4w
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-7hm9-v7vf-7g4w
- Tags: area:web domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:go technique:source-review vuln:access-control vuln:info-disclosure vuln:path-traversal

**Target:** SiYuan kernel HTTP API (/api/av/* attribute-view endpoints)
**Root cause:** Identifier validation (ast.IsNodeIDPattern) sits only inside the not-exist/create branch of RenderAttributeView, so when the traversal path already exists the check is skipped and av.ParseAttributeView joins avID into DataDir/storage/av/ without any containment check before filelock.ReadFile.
**Steps:**
1. Enable publish mode (default port 6808); obtain a RoleReader publish token, or use anonymous access when Publish.Auth.Enable is false.
2. Send `POST /api/av/renderAttributeView` with `id` containing `../` segments resolving to an existing `.json` file outside `DataDir/storage/av/`.
3. Because the target file exists, `!filelock.IsExist(existPath)` is false, the create branch (and its `ast.IsNodeIDPattern` guard) is skipped, and `av.ParseAttributeView(avID)` runs.
4. The kernel joins `DataDir/storage/av/<avID>.json`, reads it via `filelock.ReadFile`, and unmarshals it as an attribute view.
5. Repeat against `/api/av/getAttributeViewKeysByID` (`avID`), `/api/av/getAttributeViewKeys` (`id`), `/api/av/getCurrentAttrViewImages` (`id`), which have no create branch and never reach the ID check.
**How found:** Code review of RenderAttributeView: noticed the only ast.IsNodeIDPattern guard was nested inside the `if !filelock.IsExist(existPath)` create branch, so the load path falls through to ParseAttributeView, which joins avID into the storage path with no Rel/IsSubPath check.
**Exploitation:** A publish RoleReader token (or anonymous client when publish auth is disabled) issues POST requests to the four /api/av read endpoints with a traversal id; the kernel reads and parses the resolved .json file, returning attribute-view content from other scopes/notebooks and acting as a .json-path existence oracle. No admin role, CSRF token or write permission needed.
**Insight:** When an ID validator is placed inside a create/not-exist branch, the load path is unguarded — check whether the guard is branch-scoped and whether the sink (filepath.Join + ReadFile) enforces containment; also check sibling endpoints that lack the create branch entirely.

