# SiYuan: Anonymous publish-password authentication bypass via getHeadingChildrenDOM / getHeading*Transaction / getBacklinkDoc (publish mode)

SiYuan publish mode enforced the protected-document password only on getDoc; getHeadingChildrenDOM and sibling endpoints returned full rendered content given a leaked block ID.

- Payout: Research
- Severity: high
- Program: SiYuan
- Found by (vendor credit): Shirshakhtml
- Found by: Shirshakhtml
- Disclosed: 2026-09-03
- CVE: CVE-2026-68584
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-7j72-f6wg-cxw6
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-7j72-f6wg-cxw6
- Tags: area:web domain:api domain:web impact:data-leak kind:research platform:ghsa sev:high technique:chained technique:source-review vuln:access-control vuln:auth-bypass vuln:info-disclosure

**Target:** SiYuan kernel publish-mode HTTP API (port 6808)
**Root cause:** Publish-password enforcement (FilterContentByPublishAccess) is applied per-handler only in getDoc; other content-returning endpoints (getHeadingChildrenDOM, getHeading*Transaction, getBacklinkDoc/getBackmentionDoc) are CheckAuth-only and render any block ID regardless of the containing document's protected status.
**Steps:**
1. Confirm gate: `POST /api/filetree/getDoc` with `{"id":"PROTECTED_DOC"}` returns password-required placeholder.
2. Leak heading IDs: `POST /api/search/searchEmbedBlock` with `{"stmt":"SELECT * FROM blocks WHERE root_id='PROTECTED_DOC' AND type='h'"}` — content filtered but block IDs retained.
3. Bypass: `POST /api/block/getHeadingChildrenDOM` with `{"id":"HEADING_ID"}` returns HTTP 200 with full rendered protected body including marker.
4. Same retrieval works via getHeadingDeleteTransaction/getHeadingLevelTransaction/getHeadingInsertTransaction and getBacklinkDoc/getBackmentionDoc.
**How found:** Noticed the product documents 'protected' as password-gated, verified getDoc enforces it, then audited other content-returning handlers for the same FilterContentByPublishAccess call and found it absent; also observed FilterEmbedBlocksByPublishAccess blanks content but keeps block IDs, removing the ID precondition.
**Exploitation:** Anonymous reader on publish port 6808 with no token and no password: enumerate protected doc root ID via listDocsByPath (protected docs are publicly listed), extract internal heading IDs from searchEmbedBlock's ID-retaining filter, then call getHeadingChildrenDOM to dump the full rendered document body.
**Escalation:** Multiple independent ID-leak endpoints make the bypass untargeted and anonymous; any content endpoint omitting the check reintroduces the class.
**Insight:** When a product defines an access level enforced in one central handler, enumerate every sibling handler that returns the same resource type and diff their auth calls; also check whether 'filtered' responses retain identifiers, since IDs alone re-enable the unfiltered endpoints.

