Where
-Infinity
0
Severity
7.1
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

SiYuan versions before v3.8.2 fail to properly filter private attribute-view cell values in the getAttributeViewKeys endpoint. Publish readers can retrieve hidden KeyValues payloads from rows bound to inaccessible documents, exposing private database contents without authorization.

First published (updated )
Severity
9.2
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N

CVE: This vulnerability corresponds to 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 returning it CookieKey, NotebookCrypto, Export.PandocBin, Account, Stat, System.ID, the AI keys. That cloner is the project's own working inventory of what must not leave the server. getConf's non-administrator path maintains a separate, shorter list that has now diverged from it in three places. Any future secret added to the config will default to exposed on the reader path unless someone remembers to extend the blocklist.

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 conf object contains: cookieKey — the session-signing key, cleartext export.pandocBin — absolute path containing the OS username notebookCrypto — encrypted-notebook key material

Differential check against the endpoint that strips them, same instance:

POST http://127.0.0.1:6808/api/system/exportConf

→ cookieKey is empty, export.pandocBin is empty, notebookCrypto is null

The same three values are 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, permitting forgery and tampering of session cookies the server validates as authentic, with administrator authentication reachable on instances that have no access-auth code configured. The same response discloses the operating-system username and workspace layout, defeating a control added specifically to prevent that disclosure, and encrypted-notebook key material.

Suggested fix

Route non-administrator getConf responses through the exportConf cloner rather than extending HideConfSecret field by field. The cloner already handles every field named here and is the list the project actually maintains; keeping two divergent inventories of the same secrets is what produced all three gaps. If a targeted patch is preferred in the interim, clear CookieKey and NotebookCrypto in HideConfSecret and add Export.PandocBin to the IsBrowserRequest block, then audit the config struct for any remaining absolute-path or secret-bearing field.

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N

CVE: This vulnerability corresponds to CVE-2026-72797.

Summary

POST /api/notebook/getEncryptedNotebookStatus returns the identifier, name, and current lock state of every encrypted notebook, with no publish-access filtering. The route is registered CheckAuth only, no CheckReadonly, no CheckAdminRole so it is reachable by the publish RoleReader token and by the anonymous account when Publish.Auth.Enable is false. Encrypted notebooks are private by design; their names frequently reveal the sensitive topic that motivated encrypting them.

Details

Route registration (kernel/api/router.go:131): go ginServer.Handle("POST", "/api/notebook/getEncryptedNotebookStatus", model.CheckAuth, getEncryptedNotebookStatus)

The handler (kernel/api/notebook.go) accepts no arguments and contains no IsReadOnlyRoleContext branch and no publish-access filter of any kind. It returns, for every encrypted notebook, {id, name, unlocked}, the notebook's box.Name and its live lock state together with enabled, count, migrationPending, migrationBoxes, and hasHistoryDependency.

Guarded-sibling asymmetry. The primary notebook listing lsNotebooks, in the same file and also reader-reachable, does filter: it skips notebooks that are Closed and any notebook whose publishAccess entry is not Visible. The project therefore publish-scopes notebook listings but getEncryptedNotebookStatus enumerates all encrypted notebooks unconditionally.

Still unfixed at HEAD. The encrypted-notebook hardening series (issue #18034 idle auto-lock, lock-on-background, key handling, atomic unlock) is local-threat work. Commit f2d966659 ("Expose encrypted notebook unlock status to plugins") introduced this exposure, and no subsequent commit gates it for the publish/reader boundary.

Secondary effect. unlocked: true is a live indicator of exactly when an encrypted notebook's plaintext is resident in memory, the window during which reader-reachable code paths that accept a notebook argument can read its decrypted content.

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 encrypted notebook exists.

POST http://127.0.0.1:6808/api/notebook/getEncryptedNotebookStatus {}

Returns the full set of encrypted notebooks with their IDs, names, and current unlocked state including notebooks that lsNotebooks withholds from the same reader session. No arguments and no privileged access are required.

Impact

An anonymous reader (publish mode with auth disabled) or any publish RoleReader learns which encrypted notebooks exist, what they are called, and whether each is currently unlocked. Notebook names are themselves sensitive: a user encrypts a notebook precisely because its subject matter is private, and the name commonly states that subject. Disclosing existence and naming to unauthenticated parties defeats that expectation, and the live lock state additionally reveals when the notebook's contents are decrypted in memory. Confidentiality-only.

Suggested fix

For IsReadOnlyRoleContext sessions, either reject the request or restrict the response to notebooks that are publish-visible, mirroring the filtering already applied in lsNotebooks. Encrypted notebooks arguably should never be enumerated to a reader at all.

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N

CVE: This vulnerability corresponds to 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":"<DOCID>"} → /MySecretNotebook/Confidential/Q3 Layoffs Plan

Identify its notebook and storage path: POST http://127.0.0.1:6808/api/filetree/getPathByID {"id":"<DOCID>"} → {"path":"/....sy","notebook":"<BOXID>"}

Enumerate IDs from a title path: POST http://127.0.0.1:6808/api/filetree/getIDsByHPath {"path":"/Confidential","notebook":"<BOXID>"} → 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.

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

SiYuan versions before v3.8.2 contain a path guard bypass vulnerability in the MCP file-access handler that uses case-sensitive matching on Linux filesystems. Attackers can read the protected publishAccess.json file by requesting case-variant paths like PublishAccess.json to disclose sensitive publish-access configuration and metadata.

First published (updated )
Severity
7.1
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

SiYuan versions before v3.8.2 contain a path traversal vulnerability in the reader-accessible file-read endpoint that follows symlinks when opening authorized asset paths. Attackers with reader role can request a logical asset under data/assets/ that is a symlink to a file outside the workspace and receive the target file bytes, bypassing workspace boundary restrictions.

First published (updated )
Severity
6.9
AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N

CVE: This vulnerability corresponds to CVE-2026-72800.

Summary

POST /api/av/getAttributeViewKeysByID returns a database's full column schema with no publish-access filtering, while its sibling getAttributeViewKeys applies the filter for reader sessions. Two further endpoints, getBlockDefIDsByRefText and getBlockRelevantIDs return workspace-wide block IDs with no publish scoping. 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.

Details

(a) getAttributeViewKeysByID: ungated column-schema disclosure

The entire handler (kernel/api/av.go, router line 548): go ret.Data = model.GetAttributeViewKeysByID(avID, keyIDs...) // no publish gate

With an empty keyIDs, GetAttributeViewKeysByID calls av.ParseAttributeView(avID) and returns every column's av.Key:

| Field | Discloses | |---|---| | Name, Desc | column title and user-authored description | | Options | the full single/multi-select vocabulary and colours (e.g. status labels such as "Fired", "Confidential") | | Template | the column's Sprig template expression — logic and field references | | Relation | the target avID, allowing pivot to another database | | Rollup, NumberFormat, date config | further schema |

Guarded-sibling asymmetry, same file: getAttributeViewKeys (router line 526) runs FilterBlockAttributeViewKeysByPublishAccess(...) when IsReadOnlyRoleContext. getAttributeViewKeysByID applies nothing. A reader who knows an avID — trivially harvested from the data-av-id attribute of any published document embedding a database obtains the schema of any database in the workspace, including those whose host documents are hidden, password-protected, or publish-forbidden. Relation targets allow walking to sibling databases.

(b) Two unscoped block-ID enumeration oracles

Both CheckAuth-only with no publish gate. Neither returns content of its own, but both yield valid block IDs that other endpoints turn into content:

- getBlockDefIDsByRefText (router line 237) → GetBlockDefIDsByRefText(anchor) returns the block IDs, workspace-wide and including private documents, whose reference/anchor text equals a caller-supplied string: a ref-text → ID oracle. - getBlockRelevantIDs (router line 274) → GetBlockRelevantIDsInBox(id, <notebook from request>) returns parent/previous/next block IDs and traverses decrypted encrypted-notebook structure while the notebook is unlocked, a tree-walk oracle.

Verified at origin/master (eef105683): all three handler bodies contain no publish-access, publish-ignore, or readonly-role check; all three 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 database exists whose host document is publish-forbidden or password-protected.

(a) Column schema of any database: POST http://127.0.0.1:6808/api/av/getAttributeViewKeysByID {"avID":"<AVID>"} Returns every column's key object names, descriptions, select vocabularies, template expressions, and Relation target avIDs for a database whose host document the reader may not access.

Control: the sibling getAttributeViewKeys with the same avID returns filtered results for the same reader session, confirming the boundary is enforced there and omitted here.

(b) ID oracles: POST http://127.0.0.1:6808/api/block/getBlockDefIDsByRefText {"anchor":"<known ref text>"} → block IDs workspace-wide, including blocks in private documents

POST http://127.0.0.1:6808/api/block/getBlockRelevantIDs {"id":"<BLOCKID>","notebook":"<BOXID>"} → parent/previous/next block IDs; traverses encrypted-notebook structure when unlocked

Impact

An anonymous reader (publish mode with auth disabled) or any publish RoleReader can read the complete column schema of any database in the workspace, including column descriptions, select-option vocabularies (which frequently encode sensitive category labels), template logic, and relation targets that permit pivoting to further databases regardless of whether the host document is hidden, password-protected, or excluded from publishing.

The two enumeration endpoints additionally supply valid block IDs across the publish boundary, including from encrypted notebooks while unlocked. This removes the "attacker must already know a valid ID" precondition from other block-read endpoints, converting ID knowledge into content disclosure. Confidentiality-only.

Note

getAttributeViewKeysByID is a distinct handler from getAttributeView; although column definitions also appear within the latter's response payload, a fix applied to one handler does not remediate the other, and getAttributeViewKeysByID has its own filtered sibling (getAttributeViewKeys) demonstrating the intended treatment.

Suggested fix

- Gate getAttributeViewKeysByID with FilterBlockAttributeViewKeysByPublishAccess, mirroring getAttributeViewKeys. - Scope getBlockDefIDsByRefText and getBlockRelevantIDs to publish-accessible blocks for reader sessions, and ensure the InBox traversal path applies the same check before walking encrypted-notebook structure.

1 / 2
Source: GitHub
First published (updated )
Severity
8.7
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

CVE: This vulnerability corresponds to CVE-2026-72801.

Summary

Two CheckAuth-only endpoints disclose the complete offline attack material for the encrypted-notebook master password, plus the wrapped per-notebook key needed to use it. Both are reachable by the publish RoleReader token and by the anonymous account when Publish.Auth.Enable is false. An unauthenticated remote client can retrieve the Argon2id salt and cost parameters, a verifier that confirms a correct password offline, and the encrypted per-notebook data key reducing the security of every encrypted notebook to the master password's resistance to offline GPU cracking.

Details

(1) POST /api/system/getConf leaks NotebookCrypto.

getConf → GetMaskedConf() marshals the full configuration including NotebookCrypto conf.NotebookCrypto (JSON tag notebookCrypto, not -, so it survives the deep copy). For non-administrators HideConfSecret() is applied, which nulls a dozen secret-bearing fields like AI, MCPOAuth, Api, Flashcard, Publish, Repo, Sync, Secrets, Variables, System paths but contains no reference to NotebookCrypto. FilterConfByPublishIgnore() for readers only touches UILayout.

The reader therefore receives:

| Field | What it is | |---|---| | MasterSalt | global Argon2id salt | | KDFParams | Argon2id memory/time/parallelism cost | | KEKVerifier + VerifierNonce | AES-GCM-encrypted fixed magic, the in-code comment states it exists for offline master-password verification | | KEKMAC | HMAC-SHA256 of the KEK |

Either KEKVerifier or KEKMAC is a self-contained offline oracle:

KEK = Argon2id(guess, MasterSalt, KDFParams) correct if AES-GCM-decrypt(KEKVerifier, VerifierNonce) == magic or HMAC(KEK) == KEKMAC

No server round-trips are required, so there is no rate limiting, lockout, or logging on guesses, and the work is fully GPU-parallelisable.

(2) POST /api/notebook/getNotebookConf leaks the wrapped data key.

box.GetConf() returns the full BoxConf including BoxCrypt.WrappedDEK, the per-notebook data-encryption key wrapped under the KEK via AES-GCM together with WrapNonce. getNotebookInfo is the same class. Once (1) yields the master password, the attacker derives the KEK, decrypts WrappedDEK to recover the real data-encryption key, and decrypts every .sy file in that notebook.

Why this matters beyond the at-rest threat model. Storing verifier and KDF material alongside the ciphertext is reasonable against a local attacker who already has filesystem access. Serving MasterSalt + KDFParams + KEKVerifier + WrappedDEK to an anonymous remote reader converts that at-rest assumption into a remote pre-authentication cracking opportunity.

Guarded-sibling asymmetry. HideConfSecret nulls a dozen secret fields but omits NotebookCrypto. lsNotebooks filters notebook visibility for readers, while getNotebookConf and getNotebookInfo apply no reader filter at all.

Verified at origin/master (eef105683): handler bodies as described; HideConfSecret contains zero NotebookCrypto matches; FilterConfByPublishIgnore touches only UILayout; all relevant struct JSON tags are non--; all three routes are registered CheckAuth without CheckAdminRole.

Proof of Concept

Precondition: publish mode enabled (default port 6808) with at least one encrypted notebook configured; anonymous when Publish.Auth.Enable is false, otherwise any publish reader account.

1. Retrieve the key-derivation material as an anonymous reader: POST http://127.0.0.1:6808/api/system/getConf {} The response's notebookCrypto object contains MasterSalt, KDFParams, KEKVerifier, VerifierNonce, and KEKMAC while the same response has the other secret fields (Api, Repo, Sync, Publish, System paths) correctly blanked, demonstrating the omission.

2. Retrieve the wrapped notebook key: POST http://127.0.0.1:6808/api/notebook/getNotebookConf {"notebook":"<NOTEBOOKID>"} The response contains BoxCrypt.WrappedDEK and WrapNonce.

3. Offline: candidate passwords are verified locally against KEKVerifier/KEKMAC using MasterSalt and KDFParams, with no further server interaction. A recovered password yields the KEK, which unwraps WrappedDEK to the notebook's data-encryption key.

Verification status: the leak paths are confirmed by code inspection at origin/master. A live end-to-end demonstration requires a build from HEAD with an encrypted notebook enabled; the test instance available predates the encrypted-notebook feature, so no runtime reproduction is claimed here.

Impact

An unauthenticated remote client (publish mode with auth disabled) or any publish RoleReader obtains everything needed to mount an unlimited, unthrottled, GPU-parallel offline attack on the encrypted-notebook master password, plus the wrapped data key to decrypt notebook contents once the password is recovered. The confidentiality of every encrypted notebook then rests solely on master-password entropy against an offline attacker, rather than on the password remaining unknown to remote parties. No rate limiting or detection applies, because guessing occurs entirely off-server.

Suggested fix

- In HideConfSecret, replace NotebookCrypto with a minimal {enabled: bool} for non-administrators the frontend only needs the enabled flag for the lock UI stripping MasterSalt, KDFParams, KEKVerifier, VerifierNonce, and KEKMAC. - Apply reader filtering to getNotebookConf and getNotebookInfo so BoxCrypt (including WrappedDEK and WrapNonce) is omitted for non-administrator roles.

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N

CVE: This vulnerability corresponds to 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 BLOCKID under a heading with distinctive content.

Control: the gated sibling correctly refuses: POST http://127.0.0.1:6808/api/block/getBlockInfo {"id":"BLOCKID"} Blocked by checkBlockPublishAccess.

Disclosure: the ungated siblings return the data anyway: POST http://127.0.0.1:6808/api/block/getBlockBreadcrumb {"id":"BLOCKID"} → ancestor chain including the forbidden document's title and ancestor block content snippets

POST http://127.0.0.1:6808/api/block/getRefText {"id":"BLOCKID"} → the block's reference/anchor text (content of the forbidden document)

POST http://127.0.0.1:6808/api/block/getBlockTreeInfos {"ids":["BLOCKID"]} → 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.

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
Path Traversal
AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N

CVE: This vulnerability corresponds to CVE-2026-72808.

Summary

The /api/asset/getFileAnnotation endpoint returns the content of .sya PDF-annotation files with no publish-access check. It is gated by CheckAuth only, so it is reachable by the publish RoleReader token and by the anonymous account when Publish.Auth.Enable is false. Its sibling, the /assets/ asset route does enforce publish access, including the publish password. An anonymous reader who knows an asset path can therefore read the private PDF annotations (highlights, notes) attached to assets in publish-forbidden, password-protected, or unpublished documents.

Details

getFileAnnotation resolves the annotation file via GetAssetAbsPathInBox (no path traversal) and returns the .sya content. Unlike the /assets/ route, which applies the publish-access filter including password enforcement before serving asset bytes, getFileAnnotation applies no publish-access, publish-ignore, or password check. The guarded-sibling asymmetry indicates the boundary is meant to apply to this data and was omitted here.

.sya files for encrypted-box assets are fail-closed and not exposed. The gap is limited to non-encrypted assets.

Route / auth tier. getFileAnnotation is registered CheckAuth-only. CheckAuth admits RoleReader; the publish proxy forwards port-6808 traffic with a Reader JWT (anonymous when publish auth is disabled).

Proof of Concept

Reproduced on a local instance (publish mode on 6808, Basic Auth off).

Setup (admin, 6806): 1. createNotebook{name:"AnnotPoc"} → box 2. createDocWithMd{notebook, path:"/annot-victim", markdown:"doc with a pdf"} → doc 3. POST /api/asset/upload (multipart id=<doc>, file[]=@secret.pdf) → assets/secret-...pdf 4. setFileAnnotation{path:"<asset>.sya", data:"{annotSecret:ANNOTSECRET4471,note:private highlight}"} 5. setPublishAccess{id:<doc>, visible:false, password:"", disable:true} → doc forbidden

Exploit (anonymous reader, 6808, no token): POST http://127.0.0.1:6808/api/asset/getFileAnnotation {"path":"assets/secret-...pdf.sya"} Returns: {"code":0,"data":{"data":"{\"annotSecret\":\"ANNOTSECRET4471\",\"note\":\"private highlight\"}"}} The annotation content of a publish-forbidden document is returned to an anonymous reader with no publish-access check, while the /assets/ route serving the same asset class enforces publish access and password.

Impact

An anonymous reader (publish mode with auth disabled) or any publish RoleReader can read the private PDF annotations (highlights and notes) of assets belonging to publish-forbidden, password-protected, or unpublished documents, given the asset path. This defeats the publish-access/password boundary for annotation data. Scope is limited to annotated PDFs in non-encrypted notebooks; encrypted-box annotations are not exposed. Confidentiality-only.

Suggested fix

Apply the same publish-access check the /assets/ route uses to getFileAnnotation resolve the asset's owning document and enforce the publish-access/publish-ignore / password check before returning .sya content.

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:H

CVE: This vulnerability corresponds to CVE-2026-72809.

Summary

The kernel's CheckAuth grants RoleAdministrator to any request whose RemoteAddr is loopback (127.0.0.1), for a specific set of endpoints, and these localhost bypasses sit outside the accessAuthCode gate so they apply even when an access auth code is configured. This is demonstrated live (Part A below).

Separately, the fixed-port reverse proxy (fixedport.go) forwards requests to the kernel over loopback and injects no authentication token, and no SetTrustedProxies is configured, so gin does not derive the client address from forwarding headers. By code inspection, a request forwarded through this proxy would reach the kernel with RemoteAddr = 127.0.0.1. If the fixed-port proxy is bound to a network interface (via NetworkServe) and forwards remote requests to the kernel this way, the localhost bypass would grant a remote unauthenticated caller admin on those endpoints. This second step is established by reading the code but was not reproduced end-to-end, and I'm asking the maintainer to confirm the proxy's runtime forwarding behavior (Part B below).

Details

Localhost-trust admin bypass (proven). CheckAuth (session.go:298-321) contains localhost-only bypasses that key off RemoteAddr and grant RoleAdministrator. They sit outside the accessAuthCode gate i.e. they apply even when an access auth code is set and cover /api/system/exit, getNetwork, getWorkspaceInfo, /assets/, and /export/.

Fixed-port proxy behavior (code inspection). fixedport.go is a plain reverse proxy that dials the kernel at 127.0.0.1 and injects no token unlike the publish proxy, which injects a RoleReader JWT. There is no SetTrustedProxies call, so gin does not rewrite RemoteAddr from X-Forwarded-For. On this reading, a request forwarded through the fixed-port proxy reaches the kernel with RemoteAddr = 127.0.0.1.

The composition (conditional). If both hold at runtime, then: remote request → fixed-port proxy on a network interface → forwarded to kernel at 127.0.0.1 (no token) → kernel sees RemoteAddr = 127.0.0.1 → localhost bypass grants admin for the endpoints above. I have proven the final link (localhost → admin) and read the code for the forwarding link, but have not confirmed at runtime that the fixed-port proxy is instantiated and forwards remote requests as loopback in a shipped configuration.

Distinct from the previously-dismissed localhost→admin observation. That observation concerned the publish proxy path, where the injected RoleReader JWT causes CheckAuth to early-return before reaching the localhost bypasses. The fixed-port proxy injects no token, so a request through it would fall through to the RemoteAddr-keyed bypass instead. Different proxy, different code path.

Proof of Concept

Part A: the localhost bypass grants admin without auth, outside the auth-code gate (demonstrated live). On a local instance with an access auth code configured, the same no-token getWorkspaceInfo request returns different results depending on the source address the kernel sees: - From a non-loopback source (kernel sees a non-127.0.0.1 address): HTTP 401. - From 127.0.0.1 (kernel sees loopback): {"code":0,"data":{"workspaceDir":"/siyuan/workspace",…}} admin data, no auth, despite accessAuthCode being set.

This confirms the localhost-trust bypass grants admin for these endpoints and is not gated by the access auth code.

Part B: remote → proxy → loopback (code inspection only; NOT reproduced). By reading fixedport.go, the proxy dials 127.0.0.1, injects no token, and no SetTrustedProxies is set. I was not able to reproduce this end-to-end: the serve CLI in the container image tested exposes only --port and --accessAuthCode, not a flag that instantiates the fixed-port proxy in the NetworkServe-on-a-non-default-port shape, so the remote→proxy→loopback chain was not exercised at runtime. I did not invoke the destructive /api/system/exit endpoint. I'm asking the maintainer to confirm: under what conditions is the fixed-port proxy instantiated, does it bind a non-loopback interface under NetworkServe, and does it forward to the kernel preserving the client address or as loopback? That determines whether Part A's bypass is remotely reachable.

Impact

Confirmed (Part A): on any deployment where a caller can cause the kernel to see a loopback RemoteAddr, the endpoints /api/system/exit, getNetwork, getWorkspaceInfo, /assets/, and /export/ are reachable with admin rights without the access auth code i.e. the auth code does not protect these endpoints against a loopback-sourced caller. On its own this is a local/adjacent bypass of the access-code control for those endpoints. If the fixed-port proxy forwards remote requests to the kernel as loopback (to be confirmed by the maintainer), a remote unauthenticated attacker obtains those admin capabilities, remote kernel shutdown (DoS), network/workspace-path disclosure, and admin-level asset/export reads that bypass the publish-access filter. This would be a remote authentication bypass. It does not grant the full admin API only the localhost-trusted endpoints.

Suggested fix

Do not derive admin trust from RemoteAddr when a proxy forwards over loopback. Options: have the fixed-port proxy inject an explicit role/token (as the publish proxy does) so the kernel authorizes on the claim rather than the source address; or configure SetTrustedProxies and derive the real client address before applying any localhost bypass; or require the access auth code for these endpoints regardless of source address. The localhost bypass assumes loopback implies a local trusted caller, any loopback-dialing proxy breaks that assumption.

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L

CVE: This vulnerability corresponds to CVE-2026-72812.

Summary

The /api/ref/refreshBacklink endpoint is gated by CheckAuth only. Unlike its mutating siblings, it carries no CheckAdminRole, no CheckReadonly, and no inline reader-role guard so it falls through all three authorization mechanisms the codebase uses to protect write operations. A publish RoleReader or the anonymous account when Publish.Auth.Enable is false can invoke it, forcing the server to flush its pending write-transaction queue, scan all references globally, load and parse referencing trees from disk, and enqueue database writes. This violates the read-only invariant (it writes even in a globally read-only workspace), provides an unauthenticated resource-amplification/DoS primitive, and applies no per-object access check to the caller-supplied ID.

Details

Route / auth tier. router.go: Handle("POST", "/api/ref/refreshBacklink", model.CheckAuth, refreshBacklink), CheckAuth only. CheckAuth admits RoleReader; the publish proxy forwards port-6808 traffic with a Reader JWT (anonymous account when publish auth is disabled). Anonymous/reader reachable.

Guard fall-through. SiYuan protects write handlers with one of three mechanisms: route-level CheckAdminRole (AV/riff/repo/sync/setting/snippet/notebook mutations), route-level CheckReadonly (filetree/block/attr/tag mutations), or an inline IsReadOnlyRoleContext check (e.g. updateEmbedBlock, updateRecentDocTime). refreshBacklink has none of the three.

Write path reached. refreshBacklink calls model.RefreshBacklink(id): - FlushTxQueue() — forces the pending write-transaction queue to disk. - refreshRefsByDefID(defID) → QueryRefsByDefID(defID) (global scan with encrypted-box fallback loop) → filesys.LoadTrees(rootIDs) (disk read and Lute parse of every referencing tree) → sql.UpdateRefsTreeQueue(tree) (enqueues DB writes) → ref-count task update.

The handler also does not consult util.ReadOnly, so it executes its writes even when the workspace is configured globally read-only.

No per-object authorization. defID is attacker-controlled and receives no publish-access or ownership check, so a reader can force a reference reindex of any document, including publish-forbidden/unpublished ones (cross-scope).

Proof of Concept

Reproduced on a local instance (SiYuan running locally, publish mode enabled on port 6808, publish Basic Auth disabled), as an anonymous reader (no token):

Target endpoint executes the full write path: POST http://127.0.0.1:6808/api/ref/refreshBacklink {"id":"<any block id>"} Returns {"code":0,"msg":"","data":null} HTTP 200 the handler ran to completion, flushing the transaction queue and enqueuing ref writes.

Controls: the guarded mutating siblings correctly reject the same anonymous session: POST http://127.0.0.1:6808/api/tag/renameTag → 403 (CheckAdminRole and CheckReadonly) POST http://127.0.0.1:6808/api/block/foldBlock → 403 POST http://127.0.0.1:6808/api/block/updateEmbedBlock → code 0 no-op (inline IsReadOnlyRoleContext blocks the write) The 200-vs-403 contrast confirms refreshBacklink is reachable and executes where its siblings are blocked.

Impact

An anonymous reader (publish mode with auth disabled) or any publish RoleReader can:

- Bypass the read-only invariant: trigger persistent server-side writes (transaction-queue flush and reference reindex), including in a workspace configured globally read-only. - Amplify resource use without authentication: each call forces a transaction flush, a global reference scan, and disk read and parse of every referencing tree, with an attacker-controlled id and no rate limiting, a DoS primitive. - Act cross-scope: defID receives no publish-access check, so a reader can force reindexing of documents outside their publish scope. This is an integrity-invariant violation and a resource-amplification vector, not data corruption or injection, the caller cannot control the content of the writes, only trigger them. Impact is integrity-low and availability-low; no confidentiality impact and no attacker-controlled data reaches storage.

Suggested fix

Apply the same guard its mutating siblings use, add CheckReadonly (and CheckAdminRole if reference refresh is intended to be an authenticated operation) to the route, or an inline IsReadOnlyRoleContext check consistent with updateEmbedBlock. The endpoint should also honor util.ReadOnly and apply a publish-access check to defID so a reader cannot force cross-scope reindexing. More broadly, the three-way guard strategy (route middleware vs. inline check vs. none) is what allowed this handler to receive no gate at all; a structural backstop, a role-scoped route group for the mutation surface would prevent recurrence.

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N

CVE: This vulnerability corresponds to CVE-2026-68585.

Summary

The /api/block/getBlockInfo endpoint returns document root metadata including the document title (rootTitle) for a block in a publish-forbidden document, with no publish-access check. Its sibling /api/block/getDocInfo applies the publish-access filter, getBlockInfo does not. Both are gated by CheckAuth only, so getBlockInfo is reachable by the publish RoleReader token and by the anonymous account when Publish.Auth.Enable is false.

Details

The list/info side of this API is filtered while the block-info twin is not the asymmetry indicates an oversight rather than intended behavior:

| Endpoint | Returns | Publish-access filter | Route | |---|---|---|---| | getDocInfo | document info/metadata | present | CheckAuth | | getBlockInfo | box, path, rootID, rootTitle, rootChildID, rootIcon | none | CheckAuth |

getBlockInfo takes a caller-supplied block ID, validates only its format, and returns the containing document's root metadata including rootTitle (the document title) with no IsReadOnlyRoleContext / publish-access check. Because getDocInfo performs the filtering for equivalent data, the boundary is clearly meant to apply here; getBlockInfo omits it.

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 whose title is a unique marker, containing a block BLOCKID.

1. Mark the document publish-forbidden (admin action): POST http://127.0.0.1:6806/api/filetree/setPublishAccess Authorization: Token <admin-token> {"id":"DOC","visible":false,"password":"","disable":true}

2. Disclosure: the block-info endpoint returns the forbidden doc's title (anonymous, port 6808): POST http://127.0.0.1:6808/api/block/getBlockInfo {"id":"BLOCKID"} Returns HTTP 200 with data.rootTitle set to the publish-forbidden document's title, along with box, path, rootID, and rootIcon. This document's title is not returned by the reader-facing filtered paths.

Impact

An anonymous reader (publish mode with auth disabled) or any publish RoleReader can read the title and root metadata (notebook, path, root ID, icon) of a publish-forbidden document by supplying a block ID from it. This discloses the existence, title, and location of documents an administrator marked as excluded from publishing.

Precondition and scope (stated honestly): the request requires a block ID from the target document; this endpoint does not enumerate arbitrary documents. The disclosure is limited to document metadata, title, notebook, path, root ID, icon — not the document body. Block IDs for forbidden documents are obtainable from other CheckAuth-only endpoints that lack the publish-access filter (reported separately). Impact is confidentiality-only, limited to metadata; no content body, no modification. Encrypted notebooks are out of scope.

Suggested fix

Apply the same publish-access check getDocInfo uses to getBlockInfo before returning root metadata, resolve the block's document and enforce IsReadOnlyRoleContext / the publish-access filter, consistent with the sibling endpoint.

1 / 2
Source: GitHub
First published (updated )
Severity
9.9
SQL Injection, CSRF
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N

CVE: This vulnerability corresponds to 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 / queryonly, 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 loadextension).

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 "{\"embedBlockID\":\"\",\"stmt\":\"SELECT FROM blocks LIMIT 1\",\"excludeIDs\":[]}" → HTTP 200 / code:0 : arbitrary SQL executed at the anonymous reader tier. This pair is the finding.

Suggested fix

Apply the same controls the sibling SQL endpoints already use: route searchEmbedBlock through CheckSingleStatement and CheckReadonlyStatement, and gate the raw-SQL capability behind CheckAdminRole (as /api/query/sql and fullTextSearchBlock's SQL method do). At minimum, the read paths should run on a queryonly=1 handle so a reader-reachable statement cannot write or ATTACH.

1 / 2
Source: GitHub
First published (updated )
Severity
8.7
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

SiYuan versions <= 3.8.1 (fixed in v3.8.2) contain an incomplete blocklist in the IsForbiddenAbsPath() function (kernel/util/pathguard.go), which only blocks conf/conf.json by exact match and does not restrict the TLS private key (conf/key.pem) or CA private key (conf/ca.key) stored in the same conf/ directory. Because the getFile handler skips the blocklist for RoleAdministrator and all authenticated users receive RoleAdministrator in v3.8.1, any user (or any client on a default no-auth-code instance) can retrieve these private keys via POST /api/file/getFile. On deployments with TLS enabled, this allows decryption of captured HTTPS traffic (key.pem) and forging of certificates trusted by clients that imported SiYuan's CA (ca.key).

First published (updated )
Severity
9.3
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:L

SiYuan before v3.8.1 contains a stored cross-site scripting vulnerability in confirmDialog() where unescaped package names and notebook names are interpolated directly into innerHTML assignments. Attackers can submit malicious bazaar packages with HTML/script payloads in the name field that execute in users' browsers when uninstalling packages or unlocking encrypted notebooks.

First published (updated )
Severity
9.3
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:L

SiYuan before v3.8.1 fails to properly escape block name, alias, and memo fields in hint, backlink, and breadcrumb rendering functions. Attackers can set a block's name to contain HTML/script tags that execute when another user views documents referencing or displaying that block.

First published (updated )
Severity
6.9
Infoleak
AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N

SiYuan before v3.8.1 does not apply the IsForbiddenAbsPath guard (introduced in GHSA-c8r8-95hg-mp34) to the /history/path and /repo/diff/path endpoints in kernel/server/serve.go. These routes require admin authentication but construct file paths independently, so an authenticated administrator can retrieve historical snapshots of sensitive files that the guard is meant to block, including data/.siyuan/publishAccess.json (plaintext publish-mode passwords) and files under data/templates/.

First published (updated )
Severity
5.9
Path Traversal
AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:N/A:N

SiYuan 3.8.0 contains a path traversal / sensitive file exposure vulnerability in the RenderTemplate function (kernel/model/template.go), reachable via the POST /api/template/render endpoint (kernel/api/template.go). The endpoint restricts the supplied path only to the workspace directory (util.IsAbsPathInWorkspace) but, unlike the file API's refuseToAccess() blocklist, applies no sensitive-path exclusion. This allows an authenticated attacker to read sensitive workspace files, including conf/conf.json, which contains the API token and cookie signing key. The issue is fixed in v3.8.1.

First published (updated )
Severity
6.9
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

SiYuan before v3.8.1 fails to filter invisible-tier content from SQL embed blocks, attribute-view keys, and attribute-view backlinks in publish mode. Anonymous readers can enumerate invisible content through these three listing mechanisms despite admin configuration marking content unlisted.

First published (updated )
Severity
6.9
Path Traversal
AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N

SiYuan before v3.8.1 contains a path traversal vulnerability in the asset.upload MCP tool that accepts arbitrary absolute file paths without workspace boundary validation. Attackers can induce the AI Agent to upload sensitive files such as SSH keys or credentials from outside the workspace into the asset directory through prompt injection.

First published (updated )
Severity
8.4
SSRF
AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N

SiYuan versions before v3.8.1 contain a server-side request forgery vulnerability in the httprequest and webfetch agent tools that perform DNS resolution only at guard time without validating the connect-time resolution. Attackers can use DNS rebinding to answer the guard resolution with a public IP and the connect resolution with a private or metadata IP, bypassing the SSRF defense to access cloud instance metadata and internal services.

First published (updated )
Severity
7.1
AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N

SiYuan versions before 3.7.4 contain a cross-site WebSocket hijacking vulnerability in the admin-only /ws/network/proxy endpoint that explicitly disables origin validation by setting CheckOrigin to unconditionally return true. Attackers can craft malicious webpages that establish WebSocket connections to this endpoint and direct the SiYuan kernel process to proxy arbitrary network traffic to attacker-chosen targets, enabling authenticated network pivoting through the victim's machine.

First published (updated )
Severity
5.9
AV:L/AC:H/PR:L/UI:R/S:C/C:L/I:H/A:L

SiYuan versions before v3.7.4 fail to validate that packageName matches the downloaded package content in bazaar install endpoints. Attackers with same-origin access can overwrite existing trusted plugins by supplying mismatched packageName and repoURL parameters, achieving persistence across application restarts.

First published (updated )
Severity
6.9
AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N

SiYuan versions before v3.8.0 contain an incomplete path blocklist in the MCP file tool that fails to restrict access to sensitive workspace files protected by the HTTP API. Authenticated administrators can read plaintext publish-mode passwords from data/.siyuan/publishAccess.json and access other sensitive files like data/templates and data/snippets/conf.json.

First published (updated )
Severity
9.3
XSS
AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H

SiYuan before v3.7.4 contains a cross-site scripting vulnerability in the file-tree picker's hover-tooltip generation (app/src/util/pathName.ts, getLeaf()/movePathTo()) used by the 'move/link to' path-selection dialogs, where document metadata fields (bookmark, alias, memo, and an alternate name field) are concatenated into the aria-label HTML attribute without escaping. A document crafted with a double quote in any of these fields breaks out of the attribute context and injects arbitrary HTML attributes including inline event handlers (e.g., onmouseover). Because every SiYuan Electron BrowserWindow runs with nodeIntegration:true, contextIsolation:false, and no CSP, the injected handler gains require('childprocess') access, escalating the XSS to arbitrary OS command execution when a victim merely hovers over the malicious document entry in the path-picker dialog. Malicious documents reach victims via sharing, sync, or import.

First published (updated )
Severity
9.3
XSS
AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H

SiYuan through 3.7.3 contains a cross-site scripting vulnerability in the '((' block-reference autocomplete hint popup. In genHintItemHTML() (app/src/protyle/hint/extend.ts), a candidate block's name, alias, and memo fields are concatenated into the popup's HTML without escaping. An attacker who can set these metadata fields on a block can inject a self-firing payload (e.g. <img src=x onerror=...>) that executes automatically when a victim types '((' followed by a search term that surfaces the crafted block. Because SiYuan's Electron windows run with nodeIntegration enabled, contextIsolation disabled, and no CSP, the injected script gains require('childprocess') access, allowing the XSS to escalate to arbitrary OS command execution.

First published (updated )
Severity
9.4
Path Traversal
AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H

SiYuan before v3.7.4 fails to validate the packageName parameter in Bazaar install and uninstall endpoints, allowing authenticated administrators to perform path traversal via directory traversal sequences. Attackers with admin access can write arbitrary files to any location via install operations or recursively delete directories via uninstall operations by supplying crafted packageName values.

First published (updated )
Severity
8.4
Path Traversal
AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:H/A:H

SiYuan versions before v3.7.4 contain an arbitrary file deletion vulnerability in the /api/search/removeTemplate endpoint that accepts an unvalidated path parameter passed directly to os.RemoveAll. Authenticated admin attackers can supply absolute filesystem paths to recursively delete any file or directory the kernel process has permission to remove, anywhere on the host filesystem.

First published (updated )
Severity
9.2
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N

SiYuan versions before v3.7.3 contain an authentication bypass vulnerability in publish mode where content-returning endpoints getHeadingChildrenDOM, getHeadingTransaction, and getBacklinkDoc perform no password check despite protecting the primary getDoc endpoint. Anonymous attackers can retrieve full content of password-protected documents by obtaining internal block IDs from reader-accessible endpoints and calling unprotected content endpoints to bypass the password gate.

First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203