CVE-2026-73609

GHSA-j4ph-9xwf-wcj4 MEDIUM
Published October 1, 2026

### 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...

Full CISO analysis pending enrichment.

What systems are affected?

Package Ecosystem Vulnerable Range Patched
Jupyter Notebook go < 0.0.0-20260812083335-251596fc0de2 0.0.0-20260812083335-251596fc0de2
13.4K OpenSSF 5.8 3.0K dependents Pushed 8d ago 85% patched ~92d to patch Full package profile →

Do you use Jupyter Notebook? You're affected.

How severe is it?

CVSS 3.1
5.8 / 10
EPSS
0.3%
chance of exploitation in 30 days
Higher than 23% of all CVEs
Exploitation Status
No known exploitation
Sophistication
N/A

What is the attack surface?

AV AC PR UI S C I A
AV Network
AC Low
PR None
UI None
S Changed
C Low
I None
A None

What should I do?

Patch available

Update Jupyter Notebook to version 0.0.0-20260812083335-251596fc0de2

Which compliance frameworks are affected?

Compliance analysis pending. Sign in for full compliance mapping when available.

Frequently Asked Questions

What is 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: ```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.

Is CVE-2026-73609 actively exploited?

No confirmed active exploitation of CVE-2026-73609 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-73609?

Update to patched version: Jupyter Notebook 0.0.0-20260812083335-251596fc0de2.

What is the CVSS score for CVE-2026-73609?

CVE-2026-73609 has a CVSS v3.1 base score of 5.8 (MEDIUM). The EPSS exploitation probability is 0.33%.

What are the technical details?

Original Advisory

### 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.

Weaknesses (CWE)

CWE-862 — Missing Authorization: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.

  • [Architecture and Design] Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries. Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
  • [Architecture and Design] Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].

Source: MITRE CWE corpus.

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N

Timeline

Published
October 1, 2026
Last Modified
October 1, 2026
First Seen
October 1, 2026

Related Vulnerabilities