Threats affecting pkg:github/siyuan-note/siyuan
145 threats
Most recently published first
| Severity | CVSS | Signals | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|---|
| Medium | 6.9 | EPSS 0.3% | CVE-2026-72805siyuan · Missing Authorization Medium6.9EPSS 0.3%CVE Database V5CVE-2026-72805 **CVE:** This vulnerability corresponds to [CVE-2026-72805](https://nvd.nist.gov/vuln/detail/CVE-2026-72805). ### Summary Three block endpoints return document content snippets and metadata without any publish-access check, while their sibling `getBlockInfo` which returns comparable data does enforce one. All three are `CheckAuth`-only, so they are reachable by the publish `RoleReader` token and by the anonymous account when `Publish.Auth.Enable` is `false`. An anonymous reader supplying a block ID receives content and metadata belonging to publish-forbidden and password-protected documents. ### Details `getBlockInfo` (`kernel/api/block.go`) gates on the publish boundary: ```go if !checkBlockPublishAccess(c, id, ret) { return } ``` The following siblings in the same file perform no equivalent check: | Endpoint | Returns | Publish check | |---|---|---| | `getBlockInfo` | root/title/path metadata | `checkBlockPublishAccess`: present | | `getBlockBreadcrumb` | `BlockPath.Name` : root document title plus every ancestor block's content snippet | none | | `getRefText` | the block's reference/anchor text (document content) | none | | `getBlockTreeInfos` | root/title/path metadata for arbitrary block IDs | none | `getBlockBreadcrumb` returns the full ancestor chain including each ancestor block's content snippet, and `getRefText` returns block anchor text both are document content, not just metadata. `getBlockTreeInfos` returns root/title/path for any caller-supplied ID set via `model.GetBlockTreeInfosInBox(...)` with no gate. `getBlockBreadcrumb` and `getRefText` additionally accept a client-supplied notebook argument that routes to the `*InBox` variants, so the same unguarded path applies to encrypted-notebook reads while the notebook is unlocked. The correct primitive already exists in the codebase and is used by `getBlockInfo`; these three handlers simply do not call it. ### Proof of Concept Precondition: publish mode enabled (default port 6808); anonymous when `Publish.Auth.Enable` is `false`, otherwise any publish reader account. A document `D` is marked publish-forbidden (or password-protected) and contains a block `BLOCK_ID` under a heading with distinctive content. **Control: the gated sibling correctly refuses:** ``` POST http://127.0.0.1:6808/api/block/getBlockInfo {"id":"BLOCK_ID"} ``` Blocked by `checkBlockPublishAccess`. **Disclosure: the ungated siblings return the data anyway:** ``` POST http://127.0.0.1:6808/api/block/getBlockBreadcrumb {"id":"BLOCK_ID"} → ancestor chain including the forbidden document's title and ancestor block content snippets POST http://127.0.0.1:6808/api/block/getRefText {"id":"BLOCK_ID"} → the block's reference/anchor text (content of the forbidden document) POST http://127.0.0.1:6808/api/block/getBlockTreeInfos {"ids":["BLOCK_ID"]} → root ID, title, and path for the forbidden document ``` Verified by code inspection at `origin/master` (`eef10568`): `getBlockInfo` contains the `checkBlockPublishAccess` call; `getBlockBreadcrumb`, `getRefText`, and `getBlockTreeInfos` contain no publish-access, publish-ignore, or readonly-role check in their bodies. ### Impact An anonymous reader (publish mode with auth disabled) or any publish `RoleReader` can read, for documents explicitly excluded from publishing or protected by a publish password: - the document title and full ancestor chain, including ancestor block content snippets (`getBlockBreadcrumb`); - block reference/anchor text, i.e. document content (`getRefText`); - root ID, title, and path metadata for arbitrary block IDs (`getBlockTreeInfos`). Because `getBlockBreadcrumb` and `getRefText` accept a notebook argument routing to the `*InBox` variants, the same disclosure applies to encrypted notebooks while unlocked. Confidentiality-only; the precondition is a block ID, obtainable from other reader-reachable endpoints. ### Suggested fix Call `checkBlockPublishAccess` (as `getBlockInfo` does) in `getBlockBreadcrumb`, `getRefText`, and `getBlockTreeInfos` before returning data for `getBlockTreeInfos`, apply it per ID and drop unauthorized entries. Confirm the `*InBox` variants (`GetBlockRefTextInBox`, `BuildBlockBreadcrumbInBox`) enforce the same boundary so the notebook-argument path is covered. | CVE Database V5 | ||
| Medium | 6.9 | EPSS 0.3% | CVE-2026-72799siyuan · Missing Authorization Medium6.9EPSS 0.3%CVE Database V5CVE-2026-72799 **CVE:** This vulnerability corresponds to [CVE-2026-72799](https://nvd.nist.gov/vuln/detail/CVE-2026-72799). ### Summary Five filetree endpoints resolve arbitrary document IDs and paths with no publish-access check of any kind. All are `CheckAuth`-only, so they are reachable by the publish `RoleReader` token and by the anonymous account when `Publish.Auth.Enable` is `false`. An anonymous reader can map the complete private document tree every notebook, folder, and document title, and which notebook holds each document for documents marked hidden, password-protected, or publish-forbidden, and can resolve titles to document IDs. ### Details None of the following handlers invokes `IsReadOnlyRoleContext`, `CheckBlockIdAccessableByPublishAccess`, or `CheckPathAccessableByPublishIgnore`: | Endpoint | Returns | Discloses | |---|---|---| | `getFullHPathByID` (router 159) | `GetFullHPathByID(id)` | full title path including notebook, e.g. `/MySecretNotebook/Confidential/Q3 Layoffs Plan` | | `getHPathByID` (router 157) | `GetHPathByID(id)` | document-relative title path | | `getPathByID` (router 158) | `{path, notebook}` | which notebook a document lives in, plus its `.sy` storage path | | `getIDsByHPath` (router 160) | `GetIDsByHPath(path, notebook)` | title-path → document-ID enumeration | | `getHPathByPath` (router 155) | HPath from a storage path | title path from a storage path | Each accepts an arbitrary ID or HPath and resolves it identically for hidden, publish-forbidden, and password-protected documents. This enables two operations for an unauthenticated caller: 1. **Map the private document tree**: `getFullHPathByID` and `getPathByID` yield every notebook/folder/document title and its containing notebook. 2. **Resolve titles to IDs**: `getIDsByHPath` converts a known or guessed title path into document IDs, which are the required input for other block-read endpoints. Document titles and HPaths are precisely the block metadata that the project's block-metadata restriction (commit `ffde3b21e`) set out to protect; that change gated `getBlockInfo`, `getDocInfo`, and `getDocsInfo` but left this entire path-resolution family open. **Guarded-sibling asymmetry.** `getRecentDocs` (`FilterRecentDocsByPublishAccess`), `getCriteria` (`FilterCriteriaByPublishAccess`), and `getLocalStorage` (`FilterLocalStorageByPublishAccess`) all filter document references for reader sessions. The codebase clearly publish-scopes reader-visible metadata elsewhere; these five endpoints do not. Verified at `origin/master` (`eef105683`): all five handler bodies contain no publish-access call; the storage-family siblings contain their filters; all five routes are registered `CheckAuth` without `CheckAdminRole`. ### Proof of Concept Precondition: publish mode enabled (default port 6808); anonymous when `Publish.Auth.Enable` is `false`, otherwise any publish reader account. A document exists in a notebook marked publish-forbidden or password-protected. **Resolve a private document's full title path:** ``` POST http://127.0.0.1:6808/api/filetree/getFullHPathByID {"id":"<DOC_ID>"} → /MySecretNotebook/Confidential/Q3 Layoffs Plan ``` **Identify its notebook and storage path:** ``` POST http://127.0.0.1:6808/api/filetree/getPathByID {"id":"<DOC_ID>"} → {"path":"/....sy","notebook":"<BOX_ID>"} ``` **Enumerate IDs from a title path:** ``` POST http://127.0.0.1:6808/api/filetree/getIDsByHPath {"path":"/Confidential","notebook":"<BOX_ID>"} → document IDs under a folder the reader cannot otherwise access ``` Each returns data for documents excluded from publishing; no password or membership is required. ### Impact An anonymous reader (publish mode with auth disabled) or any publish `RoleReader` can enumerate the complete private document structure: notebook names, folder hierarchy, and document titles for content the administrator marked hidden, password-protected, or excluded from publishing. Document titles alone are frequently sensitive (project names, personnel actions, client identifiers). The ID-resolution direction additionally supplies valid document IDs, removing the "attacker must know an ID" precondition for other block-read endpoints. Confidentiality-only. ### Suggested fix For `IsReadOnlyRoleContext` sessions, gate each of the five handlers with `CheckBlockIdAccessableByPublishAccess` (or restrict resolution to publish-visible documents), mirroring the treatment already applied in `getRecentDocs`, `getCriteria`, and `getLocalStorage`. | CVE Database V5 | ||
| Medium | 6.9 | EPSS 0.4% | CVE-2026-72796siyuan · Missing Authorization Medium6.9EPSS 0.4%CVE Database V5CVE-2026-72796 **CVE:** This vulnerability corresponds to [CVE-2026-72796](https://nvd.nist.gov/vuln/detail/CVE-2026-72796). ### Summary Several static-file routes in the server mux (`kernel/server/serve.go`) are registered with `CheckAuth` only and serve directories directly, without the publish-access checks, sensitive-path blocklist, or `refuseToAccess` rules that the REST API applies to the same data. They are therefore reachable by the publish `RoleReader` token and by the anonymous account when `Publish.Auth.Enable` is `false`. Most notably, `/templates/` serves `data/templates` a directory the REST file API explicitly refuses to serve to non-administrators. ### Details | Route | Registration | Guard | Exposed to a reader | |---|---|---|---| | `/templates/*` (line 424) | `Group("/templates/", model.CheckAuth).Static("", data/templates)` | none beyond path cleaning | the templates directory | | `/snippets/*` (line 434) | `CheckAuth` | blocks only `conf.json` | any snippet's JS/CSS content by name | | `/widgets/`, `/plugins/`, `/emojis/` (409/414/419) | `Group(CheckAuth).Static(dir)` | none | whole directory trees | | `/export/*` (line 320) | `Group("/export/", CheckAuth)` | traversal, sensitive-path and DEK guards: **no publish check** | export artifacts (PDF/HTML/DOCX/CSV) | **`/templates/` directly contradicts an existing control.** The REST file path runs `refuseToAccess` (`kernel/api/file.go:551-553`), which explicitly returns 403 for `data/templates/` to non-administrators the project has already decided readers must not read templates. The `/templates/` static route serves that same directory to any `RoleReader`, with no `IsSensitivePath` check, no publish-access check, and no `refuseToAccess`. Templates are user-authored Markdown documents containing template/Sprig syntax and are not covered by publish-access controls. `gin`'s `.Static` disables directory listing, so a filename is required but the contradiction with `refuseToAccess` is the defect. **`/export/` versus `/assets/`.** `/assets/*path` (line 692) correctly gates non-administrators via `CheckAbsPathAccessableByPublishAccess`, plus `IsSensitivePath` and encrypted-asset handling. `/export/` carries traversal, sensitive-path and DEK guards but no publish-access check, so a reader can retrieve export artifacts of arbitrary documents, including private ones. Artifact naming is mixed, some use a random export ID, but code and CSV exports use the document name (`export/code/<name>`, `export/csv/<name>/<name>.csv`), which a reader can derive from `listDocsByPath` titles while the artifact exists. This path is therefore conditional. **Confirmed correctly gated, for contrast:** `/assets/*` (publish-gated), `/history/*` and `/repo/diff/*` (`CheckAdminRole`), `/debug/pprof/*` (disabled in production builds), `/public/` (intentionally public). All four routes above were verified to carry `CheckAuth` only, no `CheckReadonly`, no `CheckAdminRole`. ### Proof of Concept Precondition: publish mode enabled (default port 6808), anonymous when `Publish.Auth.Enable` is `false`, otherwise any publish reader account. **Templates: served despite the REST API refusing them:** ``` GET http://127.0.0.1:6808/templates/<template-file>.md → 200, template content ``` The equivalent REST request is refused: ``` POST http://127.0.0.1:6808/api/file/getFile {"path":"data/templates/<template-file>.md"} → 403 (refuseToAccess) ``` The same reader session obtains the file through the static route. **Snippets:** ``` GET http://127.0.0.1:6808/snippets/<snippet-name>.js → 200, snippet source ``` **Export artifacts (conditional on a predictable name while the artifact exists):** ``` GET http://127.0.0.1:6808/export/csv/<doc-name>/<doc-name>.csv → 200, exported content of a document the reader has no publish access to ``` ### Impact An anonymous reader (publish mode with auth disabled) or any publish `RoleReader` can read user-authored template documents that the REST API explicitly withholds from non-administrators, snippet source (JS/CSS), and the contents of the widgets, plugins and emoji directories. Where an export artifact exists under a derivable name, they can additionally retrieve exported content of documents outside their publish scope. Confidentiality-only. ### Suggested fix Apply the same treatment these routes' REST counterparts already receive: for non-administrator roles, enforce publish-access scoping plus `IsSensitivePath` and the `refuseToAccess` blocklist on the static routes, mirroring `/assets/*`. Alternatively, block `/templates/`, non-public `/snippets/`, and `/export/` for reader roles entirely. | CVE Database V5 | ||
| Critical | 9.2 | EPSS 0.3% | CVE-2026-72795siyuan · Missing Authorization Critical9.2EPSS 0.3%CVE Database V5CVE-2026-72795 **CVE:** This vulnerability corresponds to [CVE-2026-72795](https://nvd.nist.gov/vuln/detail/CVE-2026-72795). ### Summary `/api/block/getBlockDOMWithEmbed` and `/api/block/getBlockDOMsWithEmbed` gate only the *requested* block against publish access. The blocks pulled in by that block's embed (transclusion) query are inlined into the returned DOM with no publish-access check at all, so a reader who requests a legitimately-published block containing an embed query receives the content of every block that query matched including blocks in hidden, forbidden and password-protected documents. The dedicated embed endpoint `getEmbedBlock` filters these same results for reader roles. The DOM-with-embed path does not. ### Details | Item | Detail | |---|---| | Routes | `kernel/api/router.go:226` `POST /api/block/getBlockDOMWithEmbed` → `model.CheckAuth` → `getBlockDOMWithEmbed`; `:227` `POST /api/block/getBlockDOMsWithEmbed` → `model.CheckAuth` → `getBlockDOMsWithEmbed` | | Middleware | `CheckAuth` only — no `CheckReadonly`, no `CheckAdminRole` → reachable by the publish `RoleReader` token, and anonymously when `Publish.Auth.Enable` is `false` | | Guard present | Top-level requested block only. Singular: password + disable checks on the requested id. Plural: `filterBlockDOMsByPublishAccess` over the requested ids. | | Guard absent | The embedded blocks resolved from the query | | Exposed | Content HTML of every block matched by the embed query, regardless of that block's publish visibility or password tier | **The leak path.** `getBlockDOMWithEmbed` → `GetBlockDOMsWithEmbedInBox` → `resolveEmbedContentInBox` (`kernel/model/block.go:976`). That function walks the tree for `NodeBlockQueryEmbed`, extracts the embed SQL, executes `sql.SelectBlocksRawStmtInBox(stmt, ...)`, builds `embedContents` from each matched block's content HTML, and inlines the result via `SetIALAttr("embed-content", ...)`. `resolveEmbedContentInBox` takes no `publishAccess` parameter and contains no filtering: `FilterEmbedBlocks`, `CheckBlockIdAccessableByPublishAccess`, `CheckPathAccessable`, `publishAccess` and `IsReadOnlyRoleContext` all return zero matches in that function and its call path. The top-level gate therefore passes on the requested block, and the response body carries content the reader has no access to. Embed queries in published documents routinely span the entire workspace by design, dashboard documents, "all my TODOs" aggregations, tag or attribute rollups. The reader does not need to construct anything: requesting an already-published document that contains such an embed is sufficient. If the publish frontend renders embedded blocks through this endpoint, the disclosure occurs passively during normal viewing of any published document containing an embed block. **Guarded sibling.** `getEmbedBlock` (`kernel/api/router.go:212`), which is also reader-reachable and returns embed query results, applies `model.FilterEmbedBlocksByPublishAccess(c, publishAccess, blocks)` for reader roles (`kernel/model/search.go:38`). The same class of data is filtered there and unfiltered here. **Scope note.** A prior review of `getBlockDOMsWithEmbed` confirmed that the per-id publish gate is correctly applied to the *requested* ids. That observation is accurate and is not in dispute, this report concerns the *embedded* blocks resolved downstream in `resolveEmbedContentInBox`, which are not covered by that gate. ### Proof of Concept Precondition: publish mode enabled (default port 6808); anonymous when `Publish.Auth.Enable` is `false`, otherwise any publish reader account. A published document containing an embed query whose results include at least one block in a non-published or password-protected document. ``` POST http://127.0.0.1:6808/api/block/getBlockDOMWithEmbed {"id":"<id of a published block containing an embed query>"} → 200 The returned DOM contains embed-content attributes carrying the content HTML of blocks in documents the reader has no publish access to. ``` Differential check against the filtered sibling, using the same reader session and the same underlying query: ``` POST http://127.0.0.1:6808/api/search/getEmbedBlock {"stmt":"<the same embed query>"} → 200, results filtered by FilterEmbedBlocksByPublishAccess ``` The filtered endpoint withholds the private blocks; the DOM endpoint returns their content. The plural route behaves identically for each requested id: ``` POST http://127.0.0.1:6808/api/block/getBlockDOMsWithEmbed {"ids":["<published block containing an embed query>"]} ``` ### Impact An anonymous reader in publish mode or any publish `RoleReader` can read the content of blocks in documents that are not published, are marked hidden or forbidden, or are protected by a publish password, by requesting a published block that transcludes them. Because embed queries in aggregation-style published documents commonly match across the whole workspace, the volume of exposed content is bounded only by wh | CVE Database V5 | ||
| Critical | 9.2 | EPSS 0.3% | CVE-2026-72794siyuan · Insufficiently Protected Credentials Critical9.2EPSS 0.3%CVE Database V5CVE-2026-72794 **CVE:** This vulnerability corresponds to [CVE-2026-72794](https://nvd.nist.gov/vuln/detail/CVE-2026-72794). ### Summary `/api/system/getConf` returns `Conf.CookieKey`, the key used to sign the server's session cookies in its response body. The endpoint is registered with `CheckAuth` only, so the field reaches the publish `RoleReader` token and the anonymous account when `Publish.Auth.Enable` is `false`. The configuration-export endpoint in the same file strips this exact field before returning config, so the project already treats it as secret. The reader-facing masking path does not. ### Details | Item | Detail | |---|---| | Route | `kernel/api/router.go:70` `POST /api/system/getConf` → `model.CheckAuth` → `getConf` | | Middleware | `CheckAuth` only — no `CheckReadonly`, no `CheckAdminRole` | | Leaked field | `AppConf.CookieKey`, serialized as `cookieKey` | | Purpose of the field | Signing key for the `siyuan` session cookie | **The field survives every stage of the masking chain.** `getConf` masks through `GetMaskedConf()` → `HideConfSecret()` (non-administrators) → `FilterConfByPublishIgnore()` (readers) → the browser-side System-path strip. `CookieKey` is removed by none of them: - `GetMaskedConf` masks `UserData`, `MCPOAuth` and `AccessAuthCode` only. - `HideConfSecret` nulls `AI`, `Api`, `Flashcard`, `ServerAddrs`, `Publish`, `Repo`, `Sync`, `Secrets`, `Variables` and the System paths. It contains no reference to `CookieKey`. - `FilterConfByPublishIgnore` touches `UILayout` only. - The browser-side strip removes System paths only. **The key is live, not vestigial.** It is passed straight into the session store at startup: ``` cli/cmd/serve.go:67 go server.Serve(false, model.Conf.CookieKey) kernel/server/serve.go:152 sessionStore = cookie.NewStore([]byte(cookieKey)) kernel/server/serve.go:159 ginServer.Use(sessions.Sessions("siyuan", sessionStore)) ``` `gin-contrib/sessions/cookie.NewStore` constructed with a single key uses that key as the `gorilla/securecookie` HMAC key. The `siyuan` session cookie is therefore signed with the value the endpoint hands out. An attacker holding it can mint and modify session cookies that the server accepts as authentic. **Guarded sibling, in the same file.** `exportConf` (`kernel/api/system.go:299`) clones the configuration before returning it and explicitly clears both secrets: ``` kernel/api/system.go:360 clonedConf.CookieKey = "" clonedConf.NotebookCrypto = nil ``` The project has already classified `CookieKey` as a value that must not leave the server. The `getConf` masking path omits the same field. **Ceiling is environment-dependent, floor is not.** Escalating a forged session to administrator additionally requires the forged `SessionData` to carry the matching `AccessAuthCode`, which `GetMaskedConf` does mask or the instance to have no access-auth code configured, which is a common deployment. The unconditional impact, present on every instance, is the disclosure of a persistent cryptographic secret to an unauthenticated party together with the cookie-forgery capability that follows from it. Rotating the key invalidates all existing sessions, so this is not a secret that can be quietly refreshed. ### Proof of Concept Precondition: publish mode enabled (default port 6808); anonymous when `Publish.Auth.Enable` is `false`, otherwise any publish reader account. ``` POST http://127.0.0.1:6808/api/system/getConf {} → 200 The response body's conf object contains a cookieKey field holding the server's session-signing key in cleartext. ``` Differential check against the endpoint that does strip it: ``` POST http://127.0.0.1:6808/api/system/exportConf → cookieKey is empty, notebookCrypto is null ``` The same value is withheld by one endpoint and returned by the other. ### Impact An anonymous reader in publish mode or any publish `RoleReader` obtains the server's session-cookie signing key. This is a persistent cryptographic secret whose disclosure cannot be remediated without invalidating every active session. Possession of the key permits forging and tampering with `siyuan` session cookies that the server will validate as authentic. On instances with no access-auth code configured, or where a forged session's contents otherwise satisfy the server's checks, this extends to authenticating as a privileged user. ### Suggested fix Clear `CookieKey` in `HideConfSecret` for all non-administrator responses. The more durable fix is to route non-administrator `getConf` through the `exportConf` cloner, which already handles `CookieKey`, `NotebookCrypto`, `Account`, `Stat`, `System.ID`, `Export.PandocBin` and the AI keys replacing the current blocklist, which fails open on every field nobody thought to add, with the allowlist-style cloner the project already maintains. | CVE Database V5 | ||
| Critical | 9.2 | EPSS 0.3% | CVE-2026-72793siyuan · Insufficiently Protected Credentials Critical9.2EPSS 0.3%CVE Database V5CVE-2026-72793 **CVE:** This vulnerability corresponds to [CVE-2026-72793](https://nvd.nist.gov/vuln/detail/CVE-2026-72793). ### Summary `/api/system/getConf` is registered with `CheckAuth` only and is reachable by the publish `RoleReader` token, and anonymously when `Publish.Auth.Enable` is `false`. Its non-administrator masking chain is a blocklist that enumerates fields individually. Three fields that the configuration-export endpoint in the same file deliberately clears are absent from that blocklist and are returned to readers: | Field | JSON | What it is | Cleared by `exportConf` at | |---|---|---|---| | `Conf.CookieKey` | `cookieKey` | The session-cookie signing key | `kernel/api/system.go:360` | | `Conf.Export.PandocBin` | `export.pandocBin` | Absolute path embedding the OS username | `kernel/api/system.go:338` | | `Conf.NotebookCrypto` | `notebookCrypto` | Encrypted-notebook key material | `kernel/api/system.go:360` | The project has already classified all three as values that must not leave the server. The reader-facing path returns them. ### Details **Route.** `kernel/api/router.go:70` : `POST /api/system/getConf` → `model.CheckAuth` → `getConf`. No `CheckReadonly`, no `CheckAdminRole`. **The masking chain, and what each stage covers.** `getConf` masks through `GetMaskedConf()` → `HideConfSecret()` (non-administrators) → `FilterConfByPublishIgnore()` (readers) → a browser-request path strip. - `GetMaskedConf`: `UserData`, `MCPOAuth`, `AccessAuthCode`. - `HideConfSecret`: `AI`, `Api`, `Flashcard`, `ServerAddrs`, `Publish`, `Repo`, `Sync`, `Secrets`, `Variables`, and the System paths. No reference to `CookieKey` or `Export.PandocBin`. - `FilterConfByPublishIgnore`: `UILayout` only. - Browser strip (`kernel/api/system.go:630-631`): System paths only. Each stage names fields explicitly, so any field nobody thought to add is returned by default. --- **1. `CookieKey`: the live session-signing key.** The value is passed straight into the session store at startup: ``` cli/cmd/serve.go:67 go server.Serve(false, model.Conf.CookieKey) kernel/server/serve.go:152 sessionStore = cookie.NewStore([]byte(cookieKey)) kernel/server/serve.go:159 ginServer.Use(sessions.Sessions("siyuan", sessionStore)) ``` `gin-contrib/sessions/cookie.NewStore` constructed with a single key uses that key as the `gorilla/securecookie` HMAC key. The `siyuan` session cookie is signed with the value this endpoint hands out, so an attacker holding it can mint and modify session cookies the server accepts as authentic. Escalating a forged session to administrator additionally requires the forged `SessionData` to carry the matching `AccessAuthCode` which *is* masked or the instance to have no access-auth code configured, which is a common deployment. The unconditional impact, present on every instance, is disclosure of a persistent cryptographic secret to an unauthenticated party. Rotating it invalidates every active session, so it cannot be quietly refreshed. --- **2. `Export.PandocBin`: bypasses a shipped privacy control.** The field is an absolute path that embeds the OS username by construction: ``` conf/export.go:35 PandocBin string `json:"pandocBin"` model/conf.go:430-431 if "" == Conf.Export.PandocBin { Conf.Export.PandocBin = util.PandocBinPath } util/pandoc.go:154 PandocBinPath = filepath.Join(tempPandocDir, "bin", "pandoc.exe") util/pandoc.go:134 tempPandocDir = filepath.Join(TempDir, "pandoc") util/working.go:359 TempDir = filepath.Join(WorkspaceDir, "temp") util/working.go:312 defaultWorkspaceDir = filepath.Join(userProfile, "SiYuan") ``` → `C:\Users\<username>\SiYuan\temp\pandoc\bin\pandoc.exe` This one is notable beyond the disclosure itself, because a control was shipped specifically to prevent it. `kernel/api/system.go:630` adds, for browser requests: ```go if util.IsBrowserRequest(c) { maskedConf.System.WorkspaceDir = "" maskedConf.System.AppDir = "" maskedConf.System.ConfDir = "" maskedConf.System.DataDir = "" maskedConf.System.HomeDir = "" } // 避免泄露用户名等敏感信息 ``` The comment states the goal plainly: avoid leaking the username and other sensitive information. The block enumerates only `System.*` and misses `Export.PandocBin`, which carries the same username through the same response. A publish reader is a browser request, so the System paths are blanked while `export.pandocBin` passes through intact. Where an administrator has configured a custom pandoc location, that path is disclosed instead still a filesystem-layout disclosure. --- **3. `NotebookCrypto`.** Encrypted-notebook key material is likewise absent from `HideConfSecret` while `exportConf` sets it to `nil`. Reported previously and included here only because it is the third instance of the same root cause; the fix below closes all three together. --- **The root cause is the blocklist itself.** `exportConf` (`kernel/api/system.go:299`) clones the configuration and clears each secret before | CVE Database V5 | ||
| Medium | 6.9 | EPSS 0.3% | CVE-2026-72791siyuan · Missing Authorization Medium6.9EPSS 0.3%CVE Database V5CVE-2026-72791 SiYuan v3.7.4-alpha.1 (a pre-release; the endpoint does not exist in stable v3.7.3 or earlier) contains an information disclosure vulnerability in the /api/av/getAttributeViewFieldViews endpoint. The route is registered with CheckAuth only and applies no publish-access filtering, so reader-role callers can retrieve the complete database view structure — every view's name, icon, layout type, and per-field Hidden flag — for any database whose avID is supplied, regardless of authorization. The issue was introduced by commit acfc02ee8 and fixed in v3.7.4-alpha.2. | CVE Database V5 | ||
| Medium | 6.9 | EPSS 0.3% | CVE-2026-72790siyuan · Missing Authorization Medium6.9EPSS 0.3%CVE Database V5CVE-2026-72790 SiYuan before v3.7.4 contains an information disclosure vulnerability in the /api/notebook/getNotebookInfo endpoint that returns notebook metadata without authorization checks. Attackers can read notebook names, document counts, sizes, and timestamps for closed or non-published notebooks that should be hidden from readers. | CVE Database V5 | ||
| Critical | 9.2 | EPSS 0.4% | CVE-2026-72789siyuan · Missing Authorization Critical9.2EPSS 0.4%CVE Database V5CVE-2026-72789 SiYuan before v3.7.4 fails to properly validate publish access for encrypted notebooks, treating them as publicly accessible by default. Anonymous readers can enumerate and retrieve fully decrypted document content from unlocked encrypted notebooks through the publish API without authentication or key material. | CVE Database V5 | ||
| High | 8.3 | EPSS 0.5% | CVE-2026-69086siyuan · Path Traversal High8.3EPSS 0.5%CVE Database V5CVE-2026-69086 **CVE:** This vulnerability corresponds to [CVE-2026-69086](https://nvd.nist.gov/vuln/detail/CVE-2026-69086). ### Summary Four attribute-view read endpoints build a filesystem path from a caller-controlled `id`/`avID` and read it without confining the result to the attribute-view storage directory (`DataDir/storage/av/`). On the load (file-exists) code path there is no boundary check, so an `avID` containing `../` segments escapes `storage/av/` and causes the kernel to read a `.json` file elsewhere in the workspace. The endpoints require only `CheckAuth`, which the publish service's `RoleReader` token satisfies; when `Publish.Auth.Enable` is `false` the publish proxy uses the anonymous account, making the surface reachable with no credentials. ### Details Affected endpoints (all gated by `CheckAuth` only, no `CheckAdminRole`): - `POST /api/av/renderAttributeView` → `arg["id"]` - `POST /api/av/getAttributeViewKeysByID` → `arg["avID"]` - `POST /api/av/getAttributeViewKeys` → `arg["id"]` - `POST /api/av/getCurrentAttrViewImages` → `arg["id"]` In `model.RenderAttributeView` (`model/attribute_view_render.go`), the only identifier guard `ast.IsNodeIDPattern(avID)` sits **inside** the `if !filelock.IsExist(existPath)` (create) branch: ```go existPath = GetAttributeViewDataPath(avID) // path built from avID, no check if !filelock.IsExist(existPath) { // NOT-EXIST / CREATE branch if !createIfNotExist { return // NotFound } if !ast.IsNodeIDPattern(avID) { // <-- ONLY id guard, create branch only return ErrInvalidID } // ... create ... } attrView, err = av.ParseAttributeView(avID) // LOAD runs unconditionally ``` When the traversal `avID` resolves to a file that already exists, the `!filelock.IsExist(...)` condition is `false`, the entire block (including the line with `ast.IsNodeIDPattern`) is skipped, and control falls straight through to `av.ParseAttributeView(avID)`. That function rebuilds the path via `filepath.Join(DataDir, "storage", "av", avID+".json")` and calls `filelock.ReadFile` with no `filepath.Rel` / `IsSubPath` / `..` rejection: ```go // av.ParseAttributeView -> attributeViewDataPathByBox / GetAttributeViewDataPath avJSONPath = filepath.Join(DataDir, "storage", "av", avID+".json") // no boundary check // -> parseAttributeViewByPathInBox(avJSONPath, boxID) data, _ = filelock.ReadFile(avJSONPath) // SINK ``` `filepath.Join` cleans the path but does **not** reject `..` segments, so it provides no containment. The three `getAttributeView*` endpoints call `ParseAttributeView` with no create branch at all, so they never even reach the `ast.IsNodeIDPattern` check same defect, same auth tier. The root cause is that identifier validation is placed on a single code branch rather than confining the load to the AV base directory, so the load path reads a caller-controlled location. ### PoC **Precondition:** publish mode enabled (default port `6808`); reachable by a `RoleReader` publish token, or anonymously when `Publish.Auth.Enable` is `false`. A request to `/api/av/renderAttributeView` with an `id` composed of `../` path segments that resolves to an existing `.json` file outside `DataDir/storage/av/` causes that file to be read and parsed instead of being rejected, because the identifier validation is only reached on the not-exist/create branch. I have withheld the exact encoded `id` value from this draft to avoid publishing a live traversal against internet-exposed publish instances. I'm happy to provide the precise value and a screenshot privately in this thread on request. ### Impact An authenticated publish `RoleReader` or an anonymous client when publish auth is disabled can cause the kernel to read `.json` files outside the attribute-view directory. Because the loaded file is unmarshalled into the attribute-view structure, the reliable primitives are: 1. Disclosure of attribute-view (database) content from other scopes/notebooks the reader is not authorized to see. 2. A `.json`-path existence oracle for arbitrary workspace locations. Files not conforming to the AV schema are read but reflect little content, and the `.json` suffix is force-appended, so this is **not** a general arbitrary-file read. No admin role, CSRF token, or write permission is required. ### Suggested fix Validate `avID` with `ast.IsNodeIDPattern` before path construction on **all** branches (move it ahead of `FindAttributeViewPath` / `GetAttributeViewDataPath`), or preferably, so every caller inherits it confine at the sink: in `attributeViewDataPathByBox` / `GetAttributeViewDataPath`, compute the joined path and reject it unless `filepath.Rel(avBaseDir, cleaned)` stays within `avBaseDir` (no leading `..`). Sink-side confinement also covers the three `getAttributeView*` endpoints that never reach the create-branch guard. | CVE Database V5 | ||
| Critical | 9.9 | EPSS 1.6% | CVE-2026-69084siyuan · SQL Injection Critical9.9EPSS 1.6%CVE Database V5CVE-2026-69084 **CVE:** This vulnerability corresponds to [CVE-2026-69084](https://nvd.nist.gov/vuln/detail/CVE-2026-69084). ### Summary The `/api/search/searchEmbedBlock` endpoint passes a client-supplied SQL statement verbatim to the database with no validation. The endpoint is gated by `CheckAuth` only reachable by the publish RoleReader token, and by the anonymous account when `Publish.Auth.Enable` is `false`. The statement runs on the main read-write `siyuan.db` handle through a driver that executes stacked statements, with no single-statement or read-only guard. An unauthenticated request can therefore execute arbitrary SQL reading and writing content across all cleartext notebooks. Unlike SQL injection into a fixed query, this endpoint accepts a full SQL statement by design and simply fails to restrict who may call it or what the statement may do. ### Details Data flow verbatim, unvalidated: - `searchEmbedBlock` (`kernel/api/search.go`): `stmt := arg["stmt"].(string)` passed directly to `model.SearchEmbedBlock(stmt, …)`. No validation. - `SearchEmbedBlock` → `SearchEmbedBlockInBox` → `sql.SelectBlocksRawStmtNoParse(stmt, …)` → `selectBlocksRawStmt` → `query(stmt)`. - `query()` (`kernel/sql/database.go`) calls `db.Query(stmt)` on the global `siyuan.db` handle. **Missing guards.** The comparable endpoints enforce restrictions this one omits: - `/api/query/sql` runs `CheckSingleStatement` (all modes) and `CheckReadonlyStatement` (readonly mode) and is route-gated `CheckAuth + CheckAdminRole + CheckReadonly`. - `fullTextSearchBlock` rejects the SQL search method for non-admins (`if method == 2 && !IsAdminRoleContext(c)`). `searchEmbedBlock` has none of these, no statement check, no read-only check, no admin gate. **Route/auth tier.** `router.go`: `Handle("POST", "/api/search/searchEmbedBlock", model.CheckAuth, searchEmbedBlock)`, `CheckAuth` only. `CheckAuth` admits `RoleReader`, and the publish proxy forwards port-6808 traffic with a Reader JWT (anonymous account when publish auth is disabled). Anonymous/reader reachable. **Handle/stacking.** `query()` uses the global `siyuan.db` handle, the same read-write handle behind the accepted `searchDocs` finding. Driver is the vendored `88250/go-sqlite3` (mattn fork), whose connection `query` loops over `;`-separated statements, so stacked statements execute for their side effects. The DSN sets no `mode=ro` / `_query_only`, so the handle is read-write; `ATTACH` is available. **Post-hoc filter does not bound the statement.** `FilterEmbedBlocksByPublishAccess` runs on the returned slice *after* `query()` has executed. It filters rows; it cannot constrain what the statement did. Any write or `ATTACH` side effect has already occurred before the filter runs. It is not a security boundary for this sink. **Scope.** The main `blocks`/DB handle spans every opened cleartext notebook. Encrypted notebooks use separate per-box databases and are excluded. ### Impact An unauthenticated request (publish mode with auth disabled) or any publish `RoleReader` executes arbitrary SQL on the main read-write database. This permits cross-notebook read disclosure of document content, and via the read-write handle and statement stacking, modification of database content and `ATTACH`-reachable files. No admin role, CSRF token, or write permission through the normal API is required. Encrypted notebooks are not exposed. Code execution is not reachable in the default build (no `load_extension`). ### PoC Steps 1. Build the kernel image from pinned HEAD `docker build -f D:/bb/zitadel/chatto/siyuan/Dockerfile.poc -t siyuan-head D:/bb/zitadel/chatto/siyuan` 2. Run fresh, workspace mounted to the host ``` docker rm -f siyuan-poc 2>nul docker run -d --name siyuan-poc -p 6806:6806 -p 6808:6808 -v D:/bb/siyuan:/siyuan/workspace siyuan-head serve --accessAuthCode=1234567 --port=6806 ``` Confirm it booted (version string back, not Cobra help): ``` docker logs siyuan-poc curl -s http://127.0.0.1:6806/api/system/version ``` 3. Grab the admin API token from the host file. Use that value wherever TOKEN appears below (yours was g4wj3r04ntobe9m4). 4. Turn on the reader surface: publish on 6808, Basic Auth OFF `curl -s -X POST http://127.0.0.1:6806/api/setting/setPublish -H "Content-Type: application/json" -H "Authorization: Token TOKEN" -d "{\"enable\":true,\"port\":6808,\"auth\":{\"enable\":false,\"accounts\":[]}}"` Must return "data":{"port":6808,...} (non-zero port = bound OK). Port 6808, not 6806. 5. THE PROOF: anonymous reader on 6808, no token (the differential) Guarded sibling rejects reader SQL: `curl -i -X POST http://127.0.0.1:6808/api/search/fullTextSearchBlock -H "Content-Type: application/json" -d "{\"query\":\"SELECT * FROM blocks LIMIT 1\",\"method\":2}"` → -1 / "SQL search requires administrator privileges" searchEmbedBlock accepts the same reader SQL (no guard): `curl -i -X POST http://127.0.0.1:6808/api/search/searchEmbedBlock -H "Content-Type: application/json" -d "{\"embedB | CVE Database V5 | ||
| Critical | 9.2 | EPSS 0.4% | CVE-2026-68586siyuan · Missing Authorization Critical9.2EPSS 0.4%CVE Database V5CVE-2026-68586 **CVE:** This vulnerability corresponds to [CVE-2026-68586](https://nvd.nist.gov/vuln/detail/CVE-2026-68586). ### Summary The backlink API splits into list endpoints (which documents reference a block) and content endpoints (the rendered text of those referencing blocks). The list endpoints apply a publish-access filter, the content endpoints do not. As a result, `/api/ref/getBacklinkDoc` and `/api/ref/getBackmentionDoc` return the rendered DOM of blocks belonging to a publish-forbidden document to an anonymous reader, with no access check. Both content endpoints are gated by `CheckAuth` only, reachable by the publish `RoleReader` token and by the anonymous account when `Publish.Auth.Enable` is `false`. ### Details The asymmetry between the list and content sides is the tell that this is an oversight, not intended behavior: | Endpoint | Returns | Publish-access filter | Route | |---|---|---|---| | `getBacklink` | list (`*Path`) | `FilterPathsByPublishAccess` - present | `CheckAuth` | | `getBacklink2` | list (`*Path`) | `FilterPathsByPublishAccess` - present | `CheckAuth` | | `getBacklinkDoc` | rendered DOM | none | `CheckAuth` | | `getBackmentionDoc` | rendered DOM | none | `CheckAuth` | `model/backlink.go` contains no publish-access reference anywhere, and `Backlink.DOM` is the rendered HTML of the referencing blocks. `getBacklinkDoc(defID, refTreeID)` returns the rendered content of blocks in `refTreeID` - including a publish-forbidden, publish-disabled, or password-protected document with no access check. The filtered list siblings (`getBacklink`/`getBacklink2`) demonstrate that the publish boundary is meant to apply to this data; the content endpoints simply omit it. A reader is not limited to the filtered backlink list, they call `getBacklinkDoc` directly with any `refTreeID`. This also yields a reference-existence oracle: the response reveals whether the forbidden document `refTreeID` references the block `defID`. ### Proof of Concept Reproduced on a local instance (SiYuan running locally, publish mode enabled on port 6808, publish Basic Auth disabled). Setup: a publish-forbidden document `D` (`REFTREEID`) whose body contains the unique marker `SECRET_MARKER_77` and which references a block `DEFID` in a separate known document. **1. Mark the target document publish-forbidden (admin action, the boundary that should block reads):** ``` POST http://127.0.0.1:6806/api/filetree/setPublishAccess Authorization: Token <admin-token> {"id":"REFTREEID","visible":false,"password":"","disable":true} ``` **2. Baseline: the list endpoint correctly hides the forbidden doc from the reader (anonymous, port 6808):** ``` POST http://127.0.0.1:6808/api/ref/getBacklink2 {"id":"DEFID","k":"","mk":""} ``` The returned backlinks do not include the forbidden document `D`, the list side is filtered. **3. Disclosure: the content endpoint returns the forbidden doc's blocks anyway (anonymous, port 6808):** ``` POST http://127.0.0.1:6808/api/ref/getBacklinkDoc {"defID":"DEFID","refTreeID":"REFTREEID","keyword":""} ``` Returns HTTP 200; `data.backlinks[].dom` contains `SECRET_MARKER_77`, the rendered content of the publish-forbidden document, returned to an anonymous reader. `getBackmentionDoc` behaves identically for mention-type references. ### Impact An anonymous reader (publish mode with auth disabled) or any publish `RoleReader`, can read the rendered content of a publish-forbidden document's referencing blocks, defeating a boundary the administrator explicitly configured, and can determine whether a forbidden document references a given block (a reference-existence oracle). **Precondition (stated honestly):** the request requires `refTreeID` (the forbidden document's ID) and `defID` (a block it references). `defID` may be any published/known block, so if the forbidden document references any public content, `defID` is known and only `refTreeID` need be supplied. Block/document IDs for forbidden documents are also obtainable from other `CheckAuth`-only endpoints that lack the publish-access filter (reported separately). This endpoint alone does not enumerate arbitrary documents; it discloses content once an ID is known. Impact is confidentiality-only: content disclosure plus a reference-existence oracle, no modification. No admin role, CSRF token, or write permission is required. Encrypted notebooks are out of scope. ### Suggested fix Apply the same publish-access check the list siblings use. In `getBacklinkDoc`/`getBackmentionDoc`, filter each `Backlink` by its source document's box/path via `CheckPathAccessableByPublishIgnore` plus the publish-password cookie check consistent with `FilterPathsByPublishAccess` in `getBacklink`/`getBacklink2`. Any endpoint returning rendered block DOM should enforce the same publish boundary as the corresponding list endpoint. | CVE Database V5 | ||
| Critical | 9.9 | EPSS 1.4% | CVE-2026-69085siyuan · SQL Injection Critical9.9EPSS 1.4%CVE Database V5CVE-2026-69085 SiYuan before v3.7.3 contains a SQL injection vulnerability in the /api/filetree/searchDocs endpoint, where the caller-supplied keyword parameter is concatenated directly into SQL statements with no escaping or parameter binding. The endpoint is reachable by a publish RoleReader token, or unauthenticated when publish mode is enabled with Publish.Auth.Enable set to false. Because the statement executes on a read-write SQLite handle via a driver that supports stacked (semicolon-separated) statements, an attacker can read and modify database content across all cleartext (non-encrypted) notebooks on the instance. | CVE Database V5 | ||
| Critical | 10.0 | EPSS 0.8% | CVE-2026-66012siyuan · Missing Authorization Critical10.0EPSS 0.8%CVE Database V5CVE-2026-66012 SiYuan before v3.7.2 contains a missing authorization vulnerability in the POST /mcp kernel endpoint, which is gated only by a general auth check (model.CheckAuth) with no admin-role or read-only enforcement. This exposes 31 MCP tools, including a file tool with list/read/write/delete/rename/copy actions across the entire workspace. When the Publish server is enabled in anonymous mode (Conf.Publish.Enable=true and Conf.Publish.Auth.Enable=false), the Publish reverse proxy attaches an anonymous RoleReader JWT to proxied requests, allowing a remote unauthenticated attacker to reach /mcp. The attacker can read conf/conf.json to extract accessAuthCode, api.token, and cookieKey in plaintext, write arbitrary files in the workspace, and plant a plugin into data/plugins/ that executes with nodeIntegration:true and no contextIsolation on the next desktop launch, leading to administrator takeover. | CVE Database V5 | ||
| Critical | 9.4 | EPSS 0.8% | CVE-2026-65605siyuan · Cross-site Scripting Critical9.4EPSS 0.8%CVE Database V5CVE-2026-65605 SiYuan before v3.7.2 contains a stored cross-site scripting vulnerability in Attribute View (database) cell rendering. A Template column value is rendered as HTML via text/template without auto-escaping, and EscapeHTML is only applied when HasUnclosedHtmlTag returns true; because balanced self-closing tags such as <img> are skipped by that check, a payload like <img src=x onerror=...> is stored unescaped and later inserted into the page via innerHTML, executing when the database is viewed. Because the desktop renderer runs with nodeIntegration enabled, the injected script can reach require and escalate to arbitrary command execution. | CVE Database V5 | ||
| Medium | 4.9 | EPSS 0.5% | CVE-2026-59854siyuan · Protection Mechanism Failure Medium4.9EPSS 0.5%CVE Database V5CVE-2026-59854 SiYuan is an open-source personal knowledge management system. Prior to 3.7.1, POST /api/file/globalCopyFiles accepts attacker-supplied absolute source paths and relies on util.IsSensitivePath in kernel/util/path.go, whose denylist misses common home-directory credential files such as .git-credentials, .netrc, .pgpass, .kube/config, .docker/config.json, and .gnupg, allowing an authenticated administrator or API-token user to copy those files into the workspace and exfiltrate them through the file API. This issue is fixed in versions 3.7.1-alpha.2 and 3.7.1. | CVE Database V5 | ||
| Medium | 6.5 | EPSS 0.3% | CVE-2026-59853siyuan · Missing Authorization Medium6.5EPSS 0.3%CVE Database V5CVE-2026-59853 SiYuan is an open-source personal knowledge management system. Prior to 3.7.1, the /api/storage/getCriteria endpoint returns saved search criteria from data/storage/criteria.json without the publish-access filtering used by sibling storage endpoints, allowing a publish-mode Reader to read private document paths, notebook, document, and block IDs, and search and replace keywords for unpublished documents. This issue is fixed in versions 3.7.1. | CVE Database V5 | ||
| High | 8.6 | EPSS 0.4% | CVE-2026-59855siyuan · Basic XSS High8.6EPSS 0.4%CVE Database V5CVE-2026-59855 SiYuan is an open-source personal knowledge management system. Prior to 3.7.1, Asset.render in app/src/asset/index.ts interpolates the unsanitized this.path value into HTML assigned to innerHTML, allowing a crafted asset link containing a double quote to break out of the src attribute, inject an event handler, and execute JavaScript that can run OS commands in the Electron renderer. This issue is fixed in versions 3.7.1-alpha.2 and 3.7.1. | CVE Database V5 | ||
| High | 7.7 | EPSS 0.5% | CVE-2026-59832siyuan · Path Traversal High7.7EPSS 0.5%CVE Database V5CVE-2026-59832 SiYuan is an open-source personal knowledge management system. Prior to 3.7.1, the /snippets/*filepath route handler serveSnippets in kernel/server/serve.go joins a single-decoded request path with the snippets directory without subpath containment or sensitive-path checks, allowing an authenticated request such as /snippets/%2e%2e/%2e%2e/conf/conf.json to read workspace secrets and the document database. This issue is fixed in versions 3.7.1. | CVE Database V5 | ||
| High | 8.6 | EPSS 0.5% | CVE-2026-59833siyuan · Cross-site Scripting High8.6EPSS 0.5%CVE Database V5CVE-2026-59833 SiYuan is an open-source personal knowledge management system. Prior to 3.7.1, SiYuan renders note and package content to HTML through the Lute engine with sanitization enabled, but Lute's dangerous javascript scheme block does not check form action or SVG xlink:href attributes, allowing stored cross-site scripting in document export-preview and Bazaar package README render paths that can execute OS commands in the Electron desktop renderer. This issue is fixed in versions 3.7.1. | CVE Database V5 |
Page 4 of 8