Skip to content

getBookmarkLabels returns every bookmark label in the workspace to anonymous readers, with no publish-access filtering

Moderate
88250 published GHSA-j4ph-9xwf-wcj4 Jul 29, 2026

Package

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

Affected versions

eef105683 (v3.7.3, master HEAD); present at v3.7.4-alpha.1

Patched versions

v3.7.4

Description

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.

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