CVE: This vulnerability corresponds to CVE-2026-73606.
Summary
/api/block/getRefIDs filters its results for reader roles through a helper that checks only the visibility tiers. The password tier is not checked, because the helper does not receive the request context and therefore cannot evaluate the publish auth cookie. A reader who has not entered a document's publish password learns that the document references a given block.
A function ten lines away in the same file does perform the full check, on the same input type.
Details
Route, identical at eef105683 (kernel/api/router.go:235) and dev a7ae96ce (:245):
ginServer.Handle("POST", "/api/block/getRefIDs", model.CheckAuth, getRefIDs)
No CheckReadonly, no CheckAdminRole.
The filter chain. getRefIDs (kernel/api/block.go:631) checks isEncryptedNotebookDeniedForPublish, calls model.GetBlockRefsInBox, then for read-only roles:
publishIgnore := model.GetInvisiblePublishAccess(publishAccess)
refDefs, originalRefBlockIDs = model.FilterRefDefsByPublishIgnore(publishIgnore, refDefs)
FilterRefDefsByPublishIgnore (kernel/model/publish_access.go:1324) collects the reference and definition identifiers, resolves their block trees, and delegates the decision to FilterBlockTreesByPublishIgnore (:1314), whose entire body is:
for id, bt := range bts {
if CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) {
ret[id] = bt
}
}
Inside that helper, CheckPublishAuthCookie, GetPathPasswordByPublishAccess, checkBlockTreeAccessableByPublishAccess and password all appear zero times.
The complete check exists in the same file. kernel/model/publish_access.go:431:
func checkBlockTreeAccessableByPublishAccess(c *gin.Context, publishAccess PublishAccess, bt *treenode.BlockTree) bool {
if bt == nil || IsEncryptedBoxDeniedByPublishAccess(bt.BoxID) {
return false
}
publishIgnore := filterDisablePublishAccess(publishAccess)
passwordID, password := GetPathPasswordByPublishAccess(bt.BoxID, bt.Path, publishAccess)
return CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) &&
(password == "" || CheckPublishAuthCookie(c, passwordID, password))
}
Same package, same file, same *treenode.BlockTree input. One takes the context and evaluates the password; the other does not receive it and structurally cannot.
Route-level contrast. The adjacent getChildBlocks and getTailChildBlocks both carry model.CheckAdminRole.
Note on a prior assessment. getRefIDs has been described as correctly filtered because it calls a publish-access filter. It does. The filter it calls covers the visibility tiers only.
Proof of Concept
Kernel 3.7.2, publish mode on port 6808, Publish.Auth.Enable false, anonymous client. A public document referenced from a second document whose publish tier was changed between runs. getRefIDs was called anonymously each time.
| Tier of the referring document |
Anonymous result |
| Public |
reference returned (baseline) |
| Password-protected |
reference still returned |
| Hidden |
[], filtered |
| Forbidden |
[], filtered |
The hidden and forbidden rows confirm the filter runs and works. The password-protected row is the defect.
Impact
An anonymous reader in publish mode, or any publish RoleReader, learns that a password-protected document contains a reference to a given block, without entering that document's password, and receives the block identifiers involved.
Scoped precisely: the response carries identifiers only. type RefDefs { RefID string; DefIDs []string }, returned alongside originalRefBlockIDs, a map of identifier to identifier. There is no reference text, title or content. The disclosure is the existence of a relationship, plus identifiers usable as input to other endpoints.
Confidentiality only.
Suggested fix
Thread *gin.Context into FilterRefDefsByPublishIgnore and FilterBlockTreesByPublishIgnore, and use checkBlockTreeAccessableByPublishAccess in place of the bare CheckPathAccessableByPublishIgnore call, so the password tier and the encrypted-box check are both applied.
CVE: This vulnerability corresponds to CVE-2026-73606.
Summary
/api/block/getRefIDsfilters its results for reader roles through a helper that checks only the visibility tiers. The password tier is not checked, because the helper does not receive the request context and therefore cannot evaluate the publish auth cookie. A reader who has not entered a document's publish password learns that the document references a given block.A function ten lines away in the same file does perform the full check, on the same input type.
Details
Route, identical at
eef105683(kernel/api/router.go:235) and deva7ae96ce(:245):No
CheckReadonly, noCheckAdminRole.The filter chain.
getRefIDs(kernel/api/block.go:631) checksisEncryptedNotebookDeniedForPublish, callsmodel.GetBlockRefsInBox, then for read-only roles:FilterRefDefsByPublishIgnore(kernel/model/publish_access.go:1324) collects the reference and definition identifiers, resolves their block trees, and delegates the decision toFilterBlockTreesByPublishIgnore(:1314), whose entire body is:Inside that helper,
CheckPublishAuthCookie,GetPathPasswordByPublishAccess,checkBlockTreeAccessableByPublishAccessandpasswordall appear zero times.The complete check exists in the same file.
kernel/model/publish_access.go:431:Same package, same file, same
*treenode.BlockTreeinput. One takes the context and evaluates the password; the other does not receive it and structurally cannot.Route-level contrast. The adjacent
getChildBlocksandgetTailChildBlocksboth carrymodel.CheckAdminRole.Note on a prior assessment.
getRefIDshas been described as correctly filtered because it calls a publish-access filter. It does. The filter it calls covers the visibility tiers only.Proof of Concept
Kernel 3.7.2, publish mode on port 6808,
Publish.Auth.Enablefalse, anonymous client. A public document referenced from a second document whose publish tier was changed between runs.getRefIDswas called anonymously each time.[], filtered[], filteredThe hidden and forbidden rows confirm the filter runs and works. The password-protected row is the defect.
Impact
An anonymous reader in publish mode, or any publish
RoleReader, learns that a password-protected document contains a reference to a given block, without entering that document's password, and receives the block identifiers involved.Scoped precisely: the response carries identifiers only.
type RefDefs { RefID string; DefIDs []string }, returned alongsideoriginalRefBlockIDs, a map of identifier to identifier. There is no reference text, title or content. The disclosure is the existence of a relationship, plus identifiers usable as input to other endpoints.Confidentiality only.
Suggested fix
Thread
*gin.ContextintoFilterRefDefsByPublishIgnoreandFilterBlockTreesByPublishIgnore, and usecheckBlockTreeAccessableByPublishAccessin place of the bareCheckPathAccessableByPublishIgnorecall, so the password tier and the encrypted-box check are both applied.