CVE-2026-73609 — kernel
MEDIUMCVE-2026-73609 is a medium-severity (CVSS 5.8) CWE-862 vulnerability in github.com/siyuan-note/siyuan/kernel. A fix is available for github.com/siyuan-note/siyuan/kernel — see the affected versions and patch details below.
SiYuan: getBookmarkLabels returns every bookmark label in the workspace to anonymous readers, with no publish-access filtering
Exploitation Status
No confirmed exploitation observed yet
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
- CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-73609.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
CVE-2026-73609 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 394,415 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
github.com/siyuan-note/siyuan/kernelReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
Description
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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/siyuan-note/siyuan/kernel | all versions | 0.0.0-20260812083335-251596fc0de2go get github.com/siyuan-note/siyuan/kernel@v0.0.0-20260812083335-251596fc0de2 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/siyuan-note/siyuan/kernel, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update github.com/siyuan-note/siyuan/kernel to 0.0.0-20260812083335-251596fc0de2 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-73609 is resolved across your whole dependency graph.
Workarounds
Put an independent control in front of the weakness: restrict the affected endpoint or interface to trusted networks, require an additional authentication factor or proxy-level check, and invalidate existing sessions and credentials in case the flaw has already been used.
Frequently Asked Questions
Is CVE-2026-73609 in your dependencies?
Find it across Go, including transitive dependencies.