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.
CVE: This vulnerability corresponds to CVE-2026-73048.
Summary
/api/block/getRefIDsByFileAnnotationIDis registered withCheckAuthonly 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:247on the development branch and:236on master:No
CheckAdminRole, noCheckReadonly.Handler,
kernel/api/block.go:783on dev and:628on master, byte-identical on both refs. It readsidfrom the request body and callssql.QueryRefIDsByAnnotationID(id), which executes: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, noIsReadOnlyRoleContextbranch, noCheckBlockIdAccessableByPublishAccess, 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.
getRefIDsfilters its results for reader roles. Commit74dbf857crecently corrected that filter so it evaluates the password tier as well as visibility. That commit touchedgetRefIDsandkernel/model/publish_access.goonly;getRefIDsByFileAnnotationIDdoes 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:
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.Enablefalse, 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:
The platform denies that the block exists at all.
The same block, via the annotation path:
Repeating with the second document set to the password-protected tier produces the same denial from
getBlockInfoand 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 failsCheckBlockIdAccessableByPublishAccess(c, publishAccess, refID)before assembling the response.