# SiYuan: Graph endpoints omit the publish-password tier: anonymous readers receive block-level content of password-protected documents

SiYuan graph endpoints filter only by visibility tier, so anonymous publish readers get per-block content and link topology of password-protected documents without the password.

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

**Target:** Go kernel HTTP API endpoints /api/graph/getGraph and /api/graph/getLocalGraph
**Root cause:** Graph handlers filter nodes using only the invisible-tier publish access set (GetInvisiblePublishAccess) and never evaluate the publish password tier; password-protected docs are Visible=true so their nodes pass. Nodes are block-level and set Title/Label to block.Content, exposing text.
**Steps:**
1. Enable publish mode (default port 6808); be anonymous when Publish.Auth.Enable is false, else use any publish RoleReader token.
2. Mark a document protected (visible, password set) with distinctive block content; supply no password.
3. Targeted: `POST http://127.0.0.1:6808/api/graph/getLocalGraph` with body `{"id":"<PROTECTED_DOC_ID>"}` — response nodes' Title/Label contain the protected block content plus links/backlinks.
4. Workspace-wide: `POST http://127.0.0.1:6808/api/graph/getGraph` with body `{}` — returns block-level nodes for every protected document and the full link graph.
5. Control: fetch the same doc via the normal content path; it returns the password placeholder, confirming the gate exists elsewhere but is omitted in graph code.
**How found:** Code reading: grepped graph.go for CheckPublishAuthCookie/password and found nothing; noticed both handlers use GetInvisiblePublishAccess (visibility tier only) and that FilterGraphByPublishIgnore only calls CheckPathAccessableByPublishIgnore, while protected docs are Visible=true. Then checked genTreeNodes/nodeTitleLabel and saw Title/Label = block.Content, making the leak content-level rather than title-level.
**Exploitation:** Any anonymous reader (auth disabled) or publish RoleReader calls the CheckAuth-only graph endpoints with no password cookie and harvests block content and reference topology for all protected documents in the workspace.
**Insight:** When an app has a two-tier access model (visibility + password), audit every endpoint that reuses the visibility filter helper — graph/tree/search/export endpoints often skip the second tier. Also check whether node labels are populated from block content, which upgrades a metadata leak to a content leak.

