Details
### 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:
```go
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:
```go
ret.Data = model.BookmarkLabels()
```
which reaches `kernel/model/bookmark.go:184` and then `sql.QueryBookmarkLabels()` at `kernel/sql/block_query.go:315`:
```go
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:
```go
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.
EPSS, exploit probability
Low0.33%
estimated chance of real-world exploitation in the next 30 days, higher than 23.2% of every CVE FIRST.org scores
Refreshed 10/1/2026, via FIRST.org's EPSS model, not CVSS, this measures likelihood of exploitation, not how severe it would be.