Skip to content

Publish readers learn backlink block IDs and reference counts of password-protected and publish-disabled documents via /api/block/getDocInfo and getDocsInfo

Moderate
88250 published GHSA-v758-8w88-pfr2 Sep 20, 2026

Software

github.com/siyuan-note/siyuan

Affected versions

<= 3.8.4

Patched versions

v3.8.5

Description

Summary

POST /api/block/getDocInfo and POST /api/block/getDocsInfo are registered with model.CheckAuth only (no CheckReadonly/CheckAdminRole), so a publish-mode reader (RoleReader JWT, or an anonymous visitor on a passwordless publish site) can reach them. Both handlers gate only the requested document, then return the backlink RefIDs and RefCount of that document. The publish filter FilterBlockInfoByPublishAccess clears RefIDs/RefCount only when the requested document itself is inaccessible. For a normally-published document (which the attacker deliberately requests), that branch is never taken, so the block IDs and total count of every block that backlinks into the published document are returned verbatim — including blocks that live inside password-protected and publish-disabled documents, which the publish confidentiality boundary is supposed to withhold.

Root Cause

FilterBlockInfoByPublishAccess (kernel/model/publish_access.go ~1246-1290) clears RefIDs/RefCount (lines ~1285-1286) inside the conditional if (password != "" && !authed) || !accessible evaluated against the requested doc's info.RootID (line ~1279). This only fires when the requested doc is itself inaccessible. RefIDs are populated unconditionally (kernel/model/blockinfo.go ~119-129 / ~231-243) from the workspace-wide refs table (SELECT block_id ... FROM refs WHERE def_block_root_id = ?) over the global siyuan.db, which indexes all non-encrypted notebooks regardless of publish config. disable/password are publish-config attributes (publishAccess.json), NOT index-level exclusions, so blocks inside disabled/password documents are present in the refs table and their IDs leak. The correctly-gated sibling getRefIDs runs FilterRefDefsByPublishAccess (per-element drop of refDefs in inaccessible docs), demonstrating maintainer intent that backlink source IDs are publish-sensitive; getDocInfo/getDocsInfo omit that per-element filter.

Impact

A publish reader learns the existence, block IDs (each block ID encodes a creation timestamp), and total count of blocks — located in password-protected and publish-disabled documents — that reference a published document. This is a metadata confidentiality breach of the same disclosure class as the accepted advisories GHSA-ccxp-q3vg-jvgj (getNotebookInfo doc count/size/mtime) and GHSA-2mrf-mx66-vv6g (save-path box IDs). No document content is returned (C:L).

Proof of Concept

  1. Operator publishes notebook A (contains public reference doc D_pub) and keeps notebook B password-protected or publish-disabled; a block in B backlinks into D_pub.
  2. As a publish reader (or anonymous on a passwordless site):
    POST /api/block/getDocInfo with body {"id":"<D_pub RootID>"} (or POST /api/block/getDocsInfo with {"ids":["<D_pub RootID>"],"refCount":true}).
  3. Response includes refIDs (the block IDs of the referencing blocks in B) and refCount, disclosing hidden referencing content that the publish password/disable level should have withheld.

Attack Chain

  1. Entry: Publish reader (RoleReader JWT via the publish reverse proxy, or anonymous on a passwordless publish site) sends POST /api/block/getDocInfo {"id":"<publishedDocID>"} (or getDocsInfo). Guard: CheckAuth route middleware (kernel/api/router.go:282-283). Bypass proof: CheckAuth admits RoleReader (kernel/model/session.go ~196-201); the route carries NO CheckReadonly and NO CheckAdminRole (verified on tag v3.8.4). Contrast the /api/av/* routes which carry CheckReadonly and abort RoleReader before the handler.
  2. Requested-doc gate: checkBlockPublishAccessInBox / CheckBlockIdAccessableByPublishAccess (kernel/api/block.go). Guard: rejects if the requested doc is disabled/password/invisible. Bypass proof: not bypassed — the attacker deliberately requests a legitimately published doc, which passes. This is the ONLY thing gated on this path.
  3. Filter: FilterBlockInfoByPublishAccess (kernel/model/publish_access.go ~1246-1290). Guard: clears RefIDs/RefCount at ~1285-1286. Bypass proof: that clear is inside if (password != "" && !authed) || !accessible on the requested doc's info.RootID (line ~1279). For an accessible requested doc the condition is false → RefIDs/RefCount are never touched. getRefIDs instead runs FilterRefDefsByPublishAccess which drops each refDef individually — the filter this path omits.
  4. Sink: docInfoContract returns refIDs + refCount (kernel/api/block.go ~1240; apicontract DocInfo.RefIDs/RefCount, block_document.go:7/9).
  5. Impact: Reader obtains block IDs + count of blocks inside password/disabled documents that reference the published doc.

Bypass Evidence

FilterBlockInfoByPublishAccess clears RefIDs/RefCount ONLY within the info.RootID-inaccessible branch (publish_access.go ~1276-1288); for the accessible requested doc it is a no-op on those fields. RefIDs are populated unconditionally from the workspace-wide refs SQL query (def_block_root_id = ?) over siyuan.db, which indexes password/disabled notebooks (publish config is not an index exclusion). The gated sibling getRefIDs → FilterRefDefsByPublishAccess → filterBlockTreesByPublishAccess → CheckBlockTreeDiscoverableByPublishAccess (checks disable) + password check drops every refDef in an inaccessible doc — proving the RefIDs/RefCount data is treated as publish-sensitive elsewhere in the same codebase.

Affected Versions

<= 3.8.4. Vulnerable code confirmed present on the latest release tag v3.8.4 (HEAD 9f775e8 is the v3.8.4 tag commit); no fix commit exists between the tag and HEAD.

Suggested Fix

On the getDocInfo/getDocsInfo path, filter RefIDs/RefCount per-element through the same publish-access check used by getRefIDs (FilterRefDefsByPublishAccess / per-refDef CheckBlockTreeDiscoverableByPublishAccess + password check), dropping any referencing block whose containing document is publish-disabled or password-protected — rather than only clearing the fields when the requested document itself is inaccessible.


Reported by zx (Jace) — GitHub: @manus-use

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

CVE ID

No known CVE

Weaknesses

Exposure of Sensitive Information to an Unauthorized Actor

The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. Learn more on MITRE.

Missing Authorization

The product does not perform an authorization check when an actor attempts to access a resource or perform an action. Learn more on MITRE.

Credits