Skip to content

getRefIDsByFileAnnotationID returns the identifiers of blocks citing a PDF annotation with no publish-access filtering, including blocks the same session is told do not exist

Moderate
88250 published GHSA-f3q6-vc7q-m6h7 Jul 31, 2026

Package

gomod github.com/siyuan-note/siyuan/kernel (Go)

Affected versions

eef10568 (master HEAD, v3.7.3) and dev HEAD dfc71589

Patched versions

v3.7.4

Description

CVE: This vulnerability corresponds to CVE-2026-73048.

Summary

/api/block/getRefIDsByFileAnnotationID is registered with CheckAuth only and applies no authorization of any kind. Given a file-annotation identifier it queries the workspace-wide annotation reference table and returns the identifiers of every block citing that annotation, regardless of the publish tier of the documents containing them.

The annotation identifier is rendered into the DOM of any published document that cites the same annotation, so no enumeration or guessing is required.

Details

Route, kernel/api/router.go:247 on the development branch and :236 on master:

ginServer.Handle("POST", "/api/block/getRefIDsByFileAnnotationID", model.CheckAuth, getRefIDsByFileAnnotationID)

No CheckAdminRole, no CheckReadonly.

Handler, kernel/api/block.go:783 on dev and :628 on master, byte-identical on both refs. It reads id from the request body and calls sql.QueryRefIDsByAnnotationID(id), which executes:

SELECT block_id FROM file_annotation_refs WHERE annotation_id = ?

across the workspace-wide table, and returns the result as refDefs. The statement is parameterised, so there is no injection issue. There is no notebook scoping, no IsReadOnlyRoleContext branch, no CheckBlockIdAccessableByPublishAccess, and no filter call of any kind. Because no filter runs at all, blocks in forbidden, hidden and password-protected documents are all returned equally.

Comparison with the reference-identifier path. getRefIDs filters its results for reader roles. Commit 74dbf857c recently corrected that filter so it evaluates the password tier as well as visibility. That commit touched getRefIDs and kernel/model/publish_access.go only; getRefIDsByFileAnnotationID does not appear anywhere in its diff. The two endpoints return the same kind of data, and one of them has no filter to correct.

Reachability. A published document citing a PDF highlight renders the annotation identifier directly in its served markup:

<span data-type="file-annotation-ref" data-id="assets/<file>.pdf/<annotation id>">

An anonymous reader takes that identifier from a page they are entitled to view and asks which other blocks cite it.

Proof of Concept

Publish mode enabled, Publish.Auth.Enable false, anonymous client with no credentials. Two documents cite the same PDF annotation. One is public, the other is set to the forbidden tier. Both requests were made in the same pass on the same session.

Control, establishing what this session is supposed to be denied:

POST /api/block/getBlockInfo
{"id":"<block id in the forbidden document>"}

→ {"code":-1,"msg":"Content block with id [<block id>] not found"}

The platform denies that the block exists at all.

The same block, via the annotation path:

POST /api/block/getRefIDsByFileAnnotationID
{"id":"<annotation id from the public page's DOM>"}

→ {"code":0, ... "refDefs":[
     {"refID":"<block id in the public document>"},
     {"refID":"<block id in the forbidden document>"}
   ]}

Repeating with the second document set to the password-protected tier produces the same denial from getBlockInfo and the same disclosure here, which is consistent with no filter running rather than a filter consulting the wrong list.

Impact

An anonymous reader in publish mode, or any publish RoleReader, learns that documents outside their publish scope cite a given PDF annotation, and receives the identifiers of the citing blocks. This applies to every restricted tier, including forbidden, which the product otherwise treats as absolute, and the control demonstrates that the same session is simultaneously told those blocks do not exist.

Scoped precisely: the response carries block identifiers only, with no titles, reference text or content. The disclosure is the existence of a citation relationship together with identifiers usable as input to other endpoints.

Confidentiality only, with no integrity or availability impact.

Suggested fix

Apply the same filtering the reference-identifier path now performs. When IsReadOnlyRoleContext(c) holds, drop every returned identifier that fails CheckBlockIdAccessableByPublishAccess(c, publishAccess, refID) before assembling the response.

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
Changed
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:C/C:L/I:N/A:N

CVE ID

No known CVE

Weaknesses

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