CVE: This vulnerability corresponds to CVE-2026-73609.
Summary
/api/attr/getBookmarkLabels is registered with CheckAuth only and applies no filtering of any kind. It runs a scan of the entire blocks table and returns the distinct set of every bookmark label in the workspace. An anonymous reader in publish mode receives the author's complete bookmark vocabulary, regardless of whether the bookmarked blocks live in published, hidden, password-protected or forbidden documents.
The adjacent endpoint that returns bookmarks with their blocks does filter, and does so in a way that makes the intended rule explicit: it drops a label entirely when no accessible block carries it.
Details
Route. kernel/api/router.go:297 on master, :300 on the development branch:
ginServer.Handle("POST", "/api/attr/getBookmarkLabels", model.CheckAuth, getBookmarkLabels)
No CheckAdminRole, no CheckReadonly. Reachable by the publish RoleReader token and anonymously when Publish.Auth.Enable is false.
The handler (kernel/api/attr.go) is a single line:
ret.Data = model.BookmarkLabels()
which reaches kernel/model/bookmark.go:184 and then sql.QueryBookmarkLabels() at kernel/sql/block_query.go:315:
sqlStmt := "SELECT * FROM blocks WHERE ial LIKE ?" // "%bookmark=%"
// collect distinct ialAttr(block.IAL, "bookmark") into a set, sort, return
There is no notebook scoping, no path scoping, no publish-access check and no filter function anywhere on the path. Every block in the workspace carrying a bookmark attribute contributes its label to the result.
Bookmark labels are user-authored organisational text. They routinely name the thing being organised, which is the reason they are useful and also the reason they disclose.
The guarded sibling states the rule outright. getBookmark (kernel/api/router.go:190 on master, :193 on the development branch) carries the same CheckAuth-only exposure and handles the same data. It does this:
bookmark.Blocks = model.FilterBlocksByPublishAccess(c, publishAccess, bookmark.Blocks)
bookmark.Count = len(bookmark.Blocks)
if bookmark.Count > 0 {
*tempBookmarks = append(*tempBookmarks, bookmark)
}
The final condition is the significant part. The label is not merely returned with an empty block list, it is omitted from the response entirely when no accessible block carries it. The project has therefore already decided that a reader must not learn a bookmark label they have no accessible block for. getBookmarkLabels returns those same labels unconditionally, one endpoint away.
Relationship to the tag-label fix. Commit 4515fa257 ("Enforce publish access for tag labels") established that user-authored organisational labels are data the publish boundary must protect. Bookmark labels are the same class of data, drawn from the same IAL attribute store, reached through a structurally similar handler. That commit touched kernel/api/tag.go, kernel/model/publish_access.go and its test file, and nothing in attr.go or the bookmark path, so this endpoint was not covered by it.
Worth noting that the defect here is broader than the one in the tag path. The tag filter existed and checked the visible tier, so it correctly excluded forbidden documents and leaked only the password tier. getBookmarkLabels has no filter at all, so labels from forbidden documents are returned as well.
Proof of Concept
Precondition: publish mode enabled (default port 6808), anonymous when Publish.Auth.Enable is false, otherwise any publish reader account. At least one bookmarked block in a document that is not published.
POST http://127.0.0.1:6808/api/attr/getBookmarkLabels
{}
→ 200, a sorted array of every distinct bookmark label in the workspace,
including labels used only inside documents the reader has no
publish access to
No arguments are required. For contrast, the sibling endpoint returns nothing for those same labels to the same session:
POST http://127.0.0.1:6808/api/attr/getBookmark
{}
→ 200, labels omitted entirely where no accessible block carries them
One endpoint withholds the label, the other returns it.
Impact
An anonymous reader in publish mode, or any publish RoleReader, obtains the author's complete bookmark vocabulary across the workspace. Bookmark labels are free text chosen to describe what is being organised, so the set discloses subject matter, project names, personal and organisational names, and the general shape of what the author is working on, including material in documents that are hidden, password-protected or explicitly forbidden to readers.
Scoped honestly: this returns labels only. No block identifiers, document identifiers or content are included, so it does not extend into the identifier-to-content resolution paths. Confidentiality only, with no integrity or availability impact.
Suggested fix
Mirror getBookmark. For read-only roles, build the label set from blocks that have passed FilterBlocksByPublishAccess, and emit a label only if at least one surviving block carries it. That is the rule the sibling endpoint already implements, applied to the same data.
CVE: This vulnerability corresponds to CVE-2026-73609.
Summary
/api/attr/getBookmarkLabelsis registered withCheckAuthonly and applies no filtering of any kind. It runs a scan of the entireblockstable and returns the distinct set of every bookmark label in the workspace. An anonymous reader in publish mode receives the author's complete bookmark vocabulary, regardless of whether the bookmarked blocks live in published, hidden, password-protected or forbidden documents.The adjacent endpoint that returns bookmarks with their blocks does filter, and does so in a way that makes the intended rule explicit: it drops a label entirely when no accessible block carries it.
Details
Route.
kernel/api/router.go:297on master,:300on the development branch:No
CheckAdminRole, noCheckReadonly. Reachable by the publishRoleReadertoken and anonymously whenPublish.Auth.Enableisfalse.The handler (
kernel/api/attr.go) is a single line:which reaches
kernel/model/bookmark.go:184and thensql.QueryBookmarkLabels()atkernel/sql/block_query.go:315:There is no notebook scoping, no path scoping, no publish-access check and no filter function anywhere on the path. Every block in the workspace carrying a bookmark attribute contributes its label to the result.
Bookmark labels are user-authored organisational text. They routinely name the thing being organised, which is the reason they are useful and also the reason they disclose.
The guarded sibling states the rule outright.
getBookmark(kernel/api/router.go:190on master,:193on the development branch) carries the sameCheckAuth-only exposure and handles the same data. It does this:The final condition is the significant part. The label is not merely returned with an empty block list, it is omitted from the response entirely when no accessible block carries it. The project has therefore already decided that a reader must not learn a bookmark label they have no accessible block for.
getBookmarkLabelsreturns those same labels unconditionally, one endpoint away.Relationship to the tag-label fix. Commit
4515fa257("Enforce publish access for tag labels") established that user-authored organisational labels are data the publish boundary must protect. Bookmark labels are the same class of data, drawn from the same IAL attribute store, reached through a structurally similar handler. That commit touchedkernel/api/tag.go,kernel/model/publish_access.goand its test file, and nothing inattr.goor the bookmark path, so this endpoint was not covered by it.Worth noting that the defect here is broader than the one in the tag path. The tag filter existed and checked the visible tier, so it correctly excluded forbidden documents and leaked only the password tier.
getBookmarkLabelshas no filter at all, so labels from forbidden documents are returned as well.Proof of Concept
Precondition: publish mode enabled (default port 6808), anonymous when
Publish.Auth.Enableisfalse, otherwise any publish reader account. At least one bookmarked block in a document that is not published.No arguments are required. For contrast, the sibling endpoint returns nothing for those same labels to the same session:
One endpoint withholds the label, the other returns it.
Impact
An anonymous reader in publish mode, or any publish
RoleReader, obtains the author's complete bookmark vocabulary across the workspace. Bookmark labels are free text chosen to describe what is being organised, so the set discloses subject matter, project names, personal and organisational names, and the general shape of what the author is working on, including material in documents that are hidden, password-protected or explicitly forbidden to readers.Scoped honestly: this returns labels only. No block identifiers, document identifiers or content are included, so it does not extend into the identifier-to-content resolution paths. Confidentiality only, with no integrity or availability impact.
Suggested fix
Mirror
getBookmark. For read-only roles, build the label set from blocks that have passedFilterBlocksByPublishAccess, and emit a label only if at least one surviving block carries it. That is the rule the sibling endpoint already implements, applied to the same data.