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

SiYuan from 3.6.0 before 3.8.9 contains an information disclosure vulnerability that allows unauthenticated publish visitors to bypass per-document passwords via regex search in getDoc. Attackers can send POST requests to /api/filetree/getDoc with queryMethod 3 and a broad regex to read protected body text returned in the unfiltered keywords field.

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

SiYuan before 3.8.10 contains a server-side request forgery vulnerability in the skill-install download path because fetchBytes performs only a parse-time hostname check instead of using ssrfSafeClient. Attackers can supply a skill URL via the MCP skill install action that redirects or DNS-rebinds to loopback addresses, reaching internal services and metadata endpoints.

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

SiYuan 2.1.0 before 3.8.9 contains a flaw where the loginAuth CAPTCHA challenge lives in a signed client-side cookie, allowing reuse of a solved challenge. Attackers can replay the original signed siyuan cookie with distinct wrong access codes to brute-force the code and gain an administrator session.

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

SiYuan before 3.8.9 contains an information disclosure vulnerability that allows authenticated Reader users to retrieve credential-named widget files by bypassing the sensitive-path guard in getFile. Attackers can request paths like /data/widgets/<package>/credentials.json for publish-referenced widgets to obtain API keys, tokens, or other stored secrets.

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

SiYuan 3.7.0 before 3.8.10 on Windows contains a path traversal vulnerability in kernel/util/webfetch.go that allows attackers to write files outside the import directory via backslash-encoded filenames. Attackers controlling a URL fetched by the MCP webfetch or httprequest tools can use %5C-encoded ..\ sequences to write chosen response bytes to directories above temp/import that are writable by the SiYuan user.

First published (updated )
Severity
6
Path Traversal
AV:N/AC:L/AT:P/PR:N/UI:P/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N

SiYuan 3.7.0 before 3.8.10 on Windows contains a path traversal vulnerability in kernel/util/webfetch.go that allows attackers to write files outside the import directory via backslash-encoded filenames. Attackers controlling a URL fetched by the MCP webfetch or httprequest tools can use %5C-encoded ..\ sequences to write chosen response bytes to directories above temp/import that are writable by the SiYuan user.

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

SiYuan is a personal knowledge management system. In versions 3.6.0 and below, the globalCopyFiles API eads source files using filepath.Abs() with no workspace boundary check, relying solely on util.IsSensitivePath() whose blocklist omits /proc/, /run/secrets/, and home directory dotfiles. An admin can copy /proc/1/environ or Docker secrets into the workspace and read them via the standard file API. An admin can exfiltrate any file readable by the SiYuan process that falls outside the incomplete blocklist. In containerized deployments this includes all injected secrets and environment variables - a common pattern for passing credentials to containers. The exfiltrated files are then accessible via the standard workspace file API and persist until manually deleted. This issue has been fixed in version 3.6.1.

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

SiYuan before 3.8.9 contains a nil pointer dereference vulnerability in privateCacheMiddleware that allows authenticated attackers to crash the kernel by reading nonexistent MCP resources. Attackers with a valid API token can send a resources/read request in an initialized MCP session, causing a panic that terminates the kernel process and interrupts service.

First published (updated )
Severity
6.9
Null Pointer Dereference
AV:N/AC:L/AT:N/PR:H/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

SiYuan before 3.8.9 contains a nil pointer dereference vulnerability in privateCacheMiddleware that allows authenticated attackers to crash the kernel by reading nonexistent MCP resources. Attackers with a valid API token can send a resources/read request in an initialized MCP session, causing a panic that terminates the kernel process and interrupts service.

First published (updated )
Severity
9.2
AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Scope note

This endpoint does not exist in v3.7.3 or on master. It was introduced on the development branch by commit 9b8e8956f on 2026-07-27 and is present on the current development head. No released stable version is affected.

Summary

/api/av/getAttributeViewSearchTarget is registered with CheckAuth only and performs no authorization of any kind. Given a database identifier and a keyword, it searches that database's rows and returns matching content, regardless of whether the caller may see the database, the rows, or the notebook holding them.

Commit 64c26e74b on 2026-07-26 closed a reader-reachable gap on getAttributeViewFieldViews by adding CheckReadonly at router line 536. 9b8e8956f, the following day, registered this endpoint at line 535 without it. The new route returns more than the one that was just closed: row content rather than field-visibility metadata.

Details

Route. kernel/api/router.go:535 on the development branch:

go ginServer.Handle("POST", "/api/av/getAttributeViewSearchTarget", model.CheckAuth, getAttributeViewSearchTarget)

No CheckReadonly, no CheckAdminRole.

Handler. kernel/api/av.go parses its arguments and calls model.GetAttributeViewSearchTarget(id, keywords). IsReadOnlyRoleContext, publishAccess and any encrypted-notebook check are all absent. The sink is kernel/model/attributeviewrender.go:69.

The adjacent routes make the omission stark. Lines 533 to 541 all handle the same data class:

| Line | Route | Guard | |---|---|---| | 534 | getAttributeViewKeys | CheckAuth, filters in-body for readers | | 535 | getAttributeViewSearchTarget | CheckAuth, nothing | | 536 | getAttributeViewFieldViews | CheckAuth, CheckReadonly | | 540 | searchAttributeView | CheckAuth, CheckReadonly | | 541 | getAttributeView | CheckAuth, CheckReadonly |

The guard at 536 is the one added by 64c26e74b. The new route sits directly above it without one.

Why this is not bounded by identifier guessing. The closest functional sibling, renderAttributeView, applies two distinct reader protections:

1. A containing-document gate, CheckAttributeViewBlockAccessableByPublishAccess, which is the full gate including CheckPublishAuthCookie. 2. Per-item filtering, FilterAttributeViewByPublishAccess, using the itemAccess and attributeAccess maps. This strips rows bound to restricted blocks out of views the reader is otherwise entitled to see.

The second layer is what makes this exploitable without guesswork. A reader takes a database block identifier from the published DOM of a page they are allowed to view, then queries this endpoint against that same database and receives precisely the rows renderAttributeView removed from the view they were served. GetAttributeViewSearchTarget reaches the same internal render function; it simply bypasses the API-layer gate.

Two aggravating properties.

getAttributeViewSearchMatches runs strings.Contains across every value of every key, and the response carries MatchedKeyID alongside the title. That is a substring oracle over every column, so a caller can test for content without needing to retrieve it wholesale.

The path also branches on IsEncryptedBox(tree.Box) into ParseAttributeViewInBox, so it reads encrypted notebooks while they are unlocked. isEncryptedNotebookDeniedForPublish appears seventeen times across the block, ref, outline, search and filetree paths, and does not appear in av.go at all.

Extraction is iterative. Each request returns a single row. Systematic recovery of a database therefore requires many requests rather than one bulk response. There is no rate limiting on the path, and the substring oracle narrows the search considerably, but I mention it so the effort is not overstated.

Proof of Concept

Precondition: a development-branch build, publish mode enabled (default port 6808), anonymous when Publish.Auth.Enable is false. A published page embedding a database whose view contains rows bound to blocks the reader cannot access.

Step 1, take the database block identifier from the published page's DOM. It is present in the served markup.

Step 2, confirm what the reader is supposed to see:

POST http://127.0.0.1:6808/api/av/renderAttributeView {"id":"<database block id>"}

→ 200, rows bound to restricted blocks are absent

Step 3, retrieve them anyway:

POST http://127.0.0.1:6808/api/av/getAttributeViewSearchTarget {"id":"<same database block id>","keywords":"<term>"}

→ 200, matching rows including those step 2 withheld, with MatchedKeyID identifying the column that matched

The differential between steps 2 and 3, on one session against one database, is the finding.

Impact

An anonymous reader in publish mode, or any publish RoleReader, reads the content of database rows that the publish filters deliberately withhold, including rows in documents that are hidden, password-protected or forbidden, and rows in encrypted notebooks while those notebooks are unlocked. The database identifier required is published in the DOM of pages the reader is entitled to view, so no enumeration or guessing is involved. The substring matching across all columns lets a caller confirm specific content without retrieving whole rows.

Confidentiality only, with no integrity or availability impact.

Suggested fix

Add CheckReadonly at router line 535, matching lines 536, 540 and 541. If read-only roles are meant to use this endpoint, apply CheckAttributeViewBlockAccessableByPublishAccess and FilterAttributeViewByPublishAccess inside the handler as renderAttributeView does, and add the isEncryptedNotebookDeniedForPublish check that the block, ref, outline, search and filetree paths already carry.

More generally, the attribute-view routes now split between middleware-gated and in-body-gated, and a new route defaults to neither. A registration-time convention, where any new /api/av/ route is CheckReadonly unless it deliberately implements in-body filtering, would prevent this recurring.

1 / 3
Source: OSV
First published (updated )
Severity
9.4
XSS
AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

SiYuan before v3.6.1 fails to sanitize package metadata and README content in the Bazaar marketplace, allowing malicious package authors to inject arbitrary HTML and JavaScript. Attackers can achieve remote code execution on any user browsing the Bazaar by embedding XSS payloads in package displayName, description, or README fields, exploiting Electron's nodeIntegration setting to execute OS commands.

First published (updated )
Severity
9.4
XSS
AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Rejected reason: This record is a duplicate; use CVE-2026-56397 instead.

1 / 2
Source: NVD
First published (updated )
Severity
5.9
XSS, CSRF, Path Traversal
AV:L/AC:H/PR:L/UI:R/S:C/C:L/I:H/A:L

Summary

installBazaarPlugin (and the equivalent endpoints for widgets/icons/templates/themes) accept packageName, repoURL, and repoHash as independent request fields with no validation that they refer to the same package. packageName alone determines the install destination; repoURL/repoHash alone determine what content gets downloaded and written there. A request can name an already-installed, already-enabled plugin as packageName while pointing repoURL/ repoHash at different, attacker-influenced content, silently overwriting the existing plugin's files.

Affected code

kernel/api/bazaar.go: go util.BindJsonArg("packageName", &packageName, true, true) util.BindJsonArg("repoURL", &repoURL, true, true) util.BindJsonArg("repoHash", &repoHash, true, true) err := model.InstallBazaarPackage("plugins", repoURL, repoHash, packageName, 0)

kernel/model/bazaar.go: go func getPackageInstallPath(pkgType, packageName string) (string, string, error) { case "plugins": return filepath.Join(util.DataDir, "plugins", packageName), "plugin.json", nil

kernel/bazaar/install.go: go func installPackage(data []byte, installPath string) (err error) { ... if err = filelock.Copy(srcPath, installPath); err != nil { return } return }

installPath is derived solely from packageName; the downloaded data is derived solely from repoURL/repoHash; nothing cross-validates them, and installPackage performs no check for whether installPath already contains an existing package before copying into it. The same pattern applies to the widget/icon/template/theme install endpoints, which share getPackageInstallPath.

This does not involve path traversal — installPath always resolves inside DataDir/plugins/ (or the corresponding directory for other package types), so this is not an instance of "arbitrary file write outside the workspace path" (excluded by this project's SECURITY.md). The issue is that the write lands in the wrong, existing location within the intended directory, not outside it — an authorization/identity-verification gap, not a path traversal.

Reachability / why this is not trivially remote

The bazaar endpoints are gated by model.CheckAuth, model.CheckAdminRole, model.CheckReadonly. When no lock-screen password is configured (the common desktop default), CheckAuth still enforces a same-origin check: go if !localhost || ... || ("" != origin && !util.IsLocalOrigin(origin)) || ... { // reject with 401 } I verified IsLocalOrigin/IsLocalHostname correctly parse the Origin header via url.Parse and check exact hostname / loopback IP: go func IsLocalOrigin(origin string) bool { if u, err := url.Parse(origin); err == nil { return IsLocalHostname(u.Hostname()) } return false } func IsLocalHostname(hostname string) bool { if "localhost" == hostname || strings.HasSuffix(hostname, ".localhost") { return true } if ip := net.ParseIP(hostname); nil != ip { return ip.IsLoopback() } return false } This was tested against common bypass patterns (subdomain-suffix tricks like localhost.attacker.com, and Origin: null as sent by sandboxed iframes) and found no bypass — this is a correctly-implemented check. This means a purely remote attacker (e.g. a malicious website) cannot invoke this endpoint via CSRF; a request must originate from SiYuan's own renderer origin.

Realistic impact

Given SiYuan runs as an Electron app with nodeIntegration: true (per prior advisories in this repository), a same-origin script-execution primitive (e.g. an XSS bug in the renderer, of which several have been fixed in this repository recently) is typically already sufficient for direct code execution. This bug's marginal value is persistence: a transient XSS-driven compromise can use this endpoint to overwrite an existing, trusted, already-enabled plugin's index.js, converting a one-time compromise into code that runs on every future launch of the application, surviving after the original XSS vector is closed.

The overwrite does not effect immediately, via this specific endpoint. PushReloadPlugin (the websocket event that triggers frontend hot-reload without restart) is only called from the batch-update path (InstallBazaarPackages, used by batchUpdatePackage), which explicitly checks if petal != nil && petal.Enabled before forcing a reload via SetPetalEnabled. The single-package install endpoint (installBazaarPlugin) does not call PushReloadPlugin at all — it only pushes a generic success toast. So an overwrite via this endpoint activates on the next app restart or manual plugin toggle, rather than instantly still a realistic and frequent event for a desktop application, but not an instant hot-reload.

Suggested fix

Verify that packageName matches the package name declared in the downloaded package's own plugin.json/theme.json/etc. before copying into installPath, and/or refuse to install over an existing, non-empty destination directory unless the request is explicitly an update of that same package (matching by source repo identity, not just by request-supplied name).

Verification notes

Confirmed via static source review of kernel/api/bazaar.go, kernel/model/bazaar.go, kernel/bazaar/install.go, kernel/model/session.go (CheckAuth), kernel/util/net.go (IsLocalOrigin/IsLocalHostname), and kernel/model/pushreload.go / app/src/plugin/loader.ts (reload trigger wiring). I do not have a running SiYuan instance in this environment to execute an end-to-end proof of concept; findings are based on tracing the actual, unmodified control flow across these files rather than assumption.

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

Summary

/api/filetree/authFilePublishAccess is registered with CheckAuth only and is reachable anonymously. It never sets a failure code, so its outcome is signalled entirely by the response message and by whether a Set-Cookie header is present. Those two signals differ across access tiers, which lets an anonymous caller determine, for any candidate document identifier, whether that document is public or nonexistent, password-protected, or exists at the hidden or forbidden tier.

Separately, hidden and forbidden entries carry an empty password. Submitting password:"" therefore satisfies the equality check for them, and the server responds by issuing Set-Cookie: publish-auth-<docID> for a document whose own tier is forbidden.

Details

Route, identical on both refs:

go ginServer.Handle("POST", "/api/filetree/authFilePublishAccess", model.CheckAuth, authFilePublishAccess)

No CheckAdminRole, no CheckReadonly.

Handler, kernel/api/filetree.go:1608 on the development branch and :1421 on master. ret.Code is never assigned on the failure path, so every response carries code:0. The branch is:

go if item.Password == password { // SetPublishAuthCookie } else { ret.Msg = Conf.Language(285) }

The cookie is issued for forbidden documents. Entries at the hidden and forbidden tiers store Password == "". A request with password:"" therefore makes item.Password == password true for them, and the handler takes the success branch. The server issues a publish-auth cookie for a document it otherwise treats as inaccessible.

The cookie itself confers nothing, because those tiers are decided before any password check elsewhere in the codebase. The defect is that the endpoint's success signal does not correspond to access being granted, and that signal is observable.

Three distinguishable outcomes. With password:"", one request per candidate identifier:

| Tier of the target | Set-Cookie | Body | |---|---|---| | Public (no publish-access entry) | absent | {"code":0,"msg":""} | | Nonexistent identifier | absent | {"code":0,"msg":""} | | Password-protected | absent | {"code":0,"msg":"Password is incorrect"} | | Hidden | present | {"code":0,"msg":""} | | Forbidden | present | {"code":0,"msg":""} |

Public and nonexistent are indistinguishable from each other, which is correct. Everything else separates. A caller learns whether a given identifier names a password-protected document, or a document that exists at the hidden or forbidden tier, which is precisely the class the publish tiers exist to conceal.

Identifiers are readily available. Document and block identifiers appear in the DOM of published pages, in reference and backlink payloads, and in annotation reference data. The caller does not need to guess them.

A fourth response on the development branch. IsEncryptedPublishRuntimeTarget sets ret.Code = -1, which identifies the supplied identifier as belonging to an encrypted notebook. That is a membership oracle for encrypted notebooks, the same data class as GHSA-f2rw-w22v-54vh.

Proof of Concept

Publish mode enabled, Publish.Auth.Enable false, anonymous client. One request per candidate identifier, all with an empty password:

POST /api/filetree/authFilePublishAccess {"id":"<candidate document id>","password":""}

Comparing the response message and the presence of Set-Cookie against the table above classifies the identifier. Documents at the hidden and forbidden tiers return a publish-auth cookie and an empty message, distinguishing them both from public documents and from identifiers that name nothing at all.

Impact

An anonymous reader in publish mode, or any publish RoleReader, can classify arbitrary document identifiers by access tier and confirm the existence of documents at the hidden and forbidden tiers. On the development branch the same endpoint additionally reveals whether an identifier belongs to an encrypted notebook.

Scoped precisely: no document content is disclosed and no privilege is gained, since the issued cookie is inert for these tiers. The value is as a confirmation primitive, turning an identifier obtained elsewhere into a definite statement about a document the reader is not permitted to see.

Confidentiality only, with no integrity or availability impact.

Suggested fix

Return an identical response for every failure and for every tier the caller may not access, with no Set-Cookie header and no distinguishing message. In particular, do not treat an empty stored password as a successful match: hidden and forbidden entries should short-circuit before the password comparison rather than satisfying it. The encrypted-notebook branch should return the same generic response as everything else rather than its own code.

A rate limit on this endpoint would be a reasonable additional hardening measure, though it does not address the disclosure above.

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

Summary

kernel/mcp/tools/file.go's resolvePath() includes an explicit comment stating its sensitive-path blocklist is meant to align with the HTTP file API's established blacklist (refuseToAccess() in kernel/api/file.go), following the remediation for GHSA-cvhv-7xhj-xjp8 ("Unauthenticated Administrator takeover exfiltrated via the MCP"), which specifically recommended restricting this tool's scope. In practice, the MCP tool implements only 1 of the 4 static blocklist entries the HTTP API enforces.

Comparison

kernel/api/file.go refuseToAccess() blocks: 1. conf/conf.json (API token, cookie signing key, accessAuthCode) 2. data/snippets/conf.json 3. the entire data/templates directory 4. data/.siyuan/publishAccess.json 5. (dynamic, per-request) any path the current publish-mode session isn't authorized for

kernel/mcp/tools/file.go resolvePath() blocks only: go confPath := filepath.Join(util.ConfDir, "conf.json") if abs == confPath { return "", fmt.Errorf("access to conf.json is forbidden") } Items 2, 3, and 4 are absent. The tool's own list/read/grep/find actions remain fully functional against them.

Impact

An MCP client with access to the file tool (post-fix for GHSA-cvhv-7xhj-xjp8, i.e. a properly authenticated Administrator-level caller) can read data/.siyuan/publishAccess.json directly:

go type PublishAccessItem struct { ID string json:"id" Visible bool json:"visible" Password string json:"password" // plaintext; empty string = no password Disable bool json:"disable" }

This exposes the plaintext access password for every publish-mode-shared notebook/document, letting the reader bypass password protection on any publish-mode share. This is a serious sibling issue given the volume of publish-mode access-control bugs already reported against this codebase — the passwords protecting exactly that feature are directly readable through a tool whose own description says "never use for workspace data."

data/templates and data/snippets/conf.json are lower-severity but still inconsistent with the HTTP API's own precedent for what counts as sensitive.

Why this is a distinct finding, not a duplicate

GHSA-cvhv-7xhj-xjp8 was about reachability an unauthenticated Reader-tier JWT being forwarded to admin-scoped paths including /mcp. That has been fixed. This report is about the tool's own scope once reached by a legitimate, properly-authenticated caller, the remediation notes for that same advisory explicitly called for restricting resolvePath to WorkspaceDir/temp/siyuan/.log, which was not implemented; instead the tool retains workspace-wide scope with only a single-file blocklist addition.

Suggested fix

Either replicate the full refuseToAccess() blocklist in resolvePath() (minimum fix, keeps parity with the HTTP API going forward by extracting refuseToAccess's path list into a shared helper both call), or implement the originally-recommended allowlist restricting the tool to WorkspaceDir/temp/siyuan/.log for read and disabling write/delete/rename/copy entirely unless the caller is Administrator, matching the tool's own advertised "debugging/log reading only" scope.

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

Summary

Two checks on the same WebSocket request URI parse it differently. The authentication exemption for keepalive sessions matches raw substrings, while the quarantine introduced to contain those sessions reads a parsed query parameter. Because Go's url.Values.Get returns only the first value for a repeated key, a single URI can satisfy the exemption and still resolve to a non-auth session identity.

The result is that an unauthenticated remote caller connects to the kernel WebSocket with a valid access auth code configured, is placed in the general broadcast pool rather than the quarantined pool, and receives the live event stream.

Details

The exemption matches substrings. kernel/server/serve.go:1222 on the development branch, :1074 on master:

go authOk = strings.Contains(s.Request.RequestURI, "/ws?app=siyuan") && strings.Contains(s.Request.RequestURI, "&id=auth&type=auth")

Neither operand is parsed. Any URI containing both literals is exempted, regardless of what the query actually resolves to.

The quarantine parses. Commit 1e370e373 added the containment for exempted sessions at kernel/util/websocket.go:171:

go id = session.Request.URL.Query().Get("id") return "auth" == id

url.Values.Get returns the first value for a repeated key.

The differential. For the URI:

/ws?app=siyuan&id=main&type=main&id=auth&type=auth

- It contains /ws?app=siyuan and &id=auth&type=auth, so authOk is true. No cookie, no JWT and no access auth code are required. - Query().Get("id") returns main, so IsAuthSession is false. AddPushChan files the session in sessions, the broadcast pool, rather than in authSessions. - Query().Get("type") returns main, so the session matches BroadcastByType("main", ...). - Without a valid X-Auth-Token the publish flag is never set, so isPublishSession() does not exclude it either.

Every guard that should have caught the session keys off a different parse than the one that admitted it.

No origin validation. CheckOrigin does not appear on the /ws path, so the cross-origin vector described in the parent advisory also remains available.

Distinction from the existing broadcast-pool advisory. GHSA-mw8r-mw84-88v2 concerns the publish port, where an anonymous session is expected to connect and the defect is that broadcast content is not gated for it afterwards. This report concerns the kernel port with an access auth code configured, where the session should never be admitted in the first place. The leaked data overlaps because both end in the same broadcast pool, but the mechanisms and the affected deployments are different, and gating publish sessions does not close this path.

Proof of Concept

Kernel port, --accessAuthCode set. Three WebSocket connections, none carrying credentials of any kind.

| Socket | Query | Result | |---|---|---| | A, baseline | ?app=siyuan&id=main&type=main | Rejected, closed 1002, 0 frames | | B, control | ?app=siyuan&id=auth&type=auth | Connects, 0 frames | | C, attack | ?app=siyuan&id=main&type=main&id=auth&type=auth | Connects, 19 to 30 frames across two runs |

Socket A establishes that the access auth code is enforced. Socket B establishes that the existing quarantine works exactly as intended, admitting the keepalive session but delivering nothing to it. Socket C differs from B only in the duplicated parameters, and receives the event stream.

Frames observed on socket C included savedoc ("action":"create","data":{"Root":{"ID": ...), transactions ("action":"updateAttrs" carrying document titles), filetreeSortChanged (the notebook's complete ordered childIDs), create (a notebook object including its name), and backgroundtask.

A canary string placed inside a document body did not appear in any frame, on a full-text search of all captured frames.

Impact

An unauthenticated remote caller obtains the kernel's live event stream on a deployment protected by an access auth code, with no user interaction and no prior knowledge of the instance.

Precisely what is disclosed: document identifiers and titles, notebook identifiers and names, file paths, the complete ordered child structure of notebooks, and the full stream of create, update and delete operations as they occur. Document body content is not present in the observed frames.

That set corresponds to the disclosure claimed by GHSA-xp2m-98x8-rpj6, which means the remediation for that advisory is bypassable rather than incomplete in scope. The absence of origin validation on the endpoint additionally preserves the cross-origin reachability described there.

Confidentiality only. No frames observed permitted writes.

Suggested fix

Make both checks agree, and stop re-parsing attacker-controlled input after admission.

1. In the exemption, parse the query rather than matching substrings, and require id and type each to equal auth exactly. 2. Reject requests carrying duplicated query keys on this endpoint, so no URI can present two identities. 3. Record the exemption as a flag on the session at connect time, and have IsAuthSession read that flag rather than re-parsing the request. A session admitted under the keepalive exemption is then quarantined by construction, independent of how its URI is read later.

Adding origin validation to /ws would additionally close the cross-origin vector.

1 / 3
Source: GitHub
First published (updated )
Severity
8.3
EPSS
0.04%
SSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L

Summary The /api/network/forwardProxy endpoint allows authenticated users to make arbitrary HTTP requests from the server. The endpoint accepts a user-controlled URL and makes HTTP requests to it, returning the full response body and headers. There is no URL validation to prevent requests to internal networks, localhost, or cloud metadata services.

Affected Code File: /kernel/api/network.go (Lines 153-317) func forwardProxy(c gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret)

arg, ok := util.JsonArg(c, ret) if !ok { return }

destURL := arg["url"].(string) // VULNERABILITY: Only validates URL format, not destination if , e := url.ParseRequestURI(destURL); nil != e { ret.Code = -1 ret.Msg = "invalid [url]" return }

// ... HTTP request is made to user-controlled URL ... resp, err := request.Send(method, destURL) // Full response body is returned to the user bodyData, err := io.ReadAll(resp.Body) // ... ret.Data = data // Contains full response body } PoC - First, authenticate with your access auth code and copy the authenticated cookie. - Now use the request below for SSRF to Access Cloud Metadata. POST /api/network/forwardProxy HTTP/1.1 Host: <HOST> Cookie: siyuan=<COOKIE> Content-Length: 102

{"url":"http://169.254.169.254/metadata/v1/","method":"GET","headers":[],"payload":"","timeout":7000}' <img width="1230" height="754" alt="Screenshot 2026-03-11 at 1 23 36 AM" src="https://github.com/user-attachments/assets/60486dba-1ccd-4287-8073-b803854756a2" />

Impact - Internal Network Reconnaissance: Attackers can scan internal services - Cloud Credential Theft: Potential access to cloud metadata and IAM credentials - Data Exfiltration: Server can be used as a proxy to access internal resources - Firewall Bypass: Requests originate from trusted internal IP

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

SiYuan before 3.8.7 contains a path traversal vulnerability in the MCP asset tool because validateAssetUploadPaths relies on an incomplete sensitive-path blacklist that omits .env and other secret files. Authenticated attackers driving the MCP asset upload action can copy such local files into the workspace and read them back via /assets/ over HTTP.

First published (updated )
Severity
6.9
Path Traversal
AV:L/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N

SiYuan before 3.8.7 contains a path traversal vulnerability in the MCP asset tool because validateAssetUploadPaths relies on an incomplete sensitive-path blacklist that omits .env and other secret files. Authenticated attackers driving the MCP asset upload action can copy such local files into the workspace and read them back via /assets/ over HTTP.

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.7 through 3.8.8 fails to normalize Windows GLOBALROOT device-namespace aliases, bypassing the IsSensitivePath denylist. Attackers can invoke importStdMd with a GLOBALROOT-prefixed path to copy the API token and TLS private key into the publicly served assets directory.

First published (updated )
Severity
5.9
Path Traversal
AV:N/AC:L/AT:P/PR:H/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N

SiYuan 3.8.7 through 3.8.8 fails to normalize Windows GLOBALROOT device-namespace aliases, bypassing the IsSensitivePath denylist. Attackers can invoke importStdMd with a GLOBALROOT-prefixed path to copy the API token and TLS private key into the publicly served assets directory.

First published (updated )
Severity
6.9
AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:L/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Scope note (please read first)

This endpoint does not exist in v3.7.3 or on master. It was introduced on the development branch by commit acfc02ee8 ("Improve database field visibility across views", #11020) and is live on v3.7.4-alpha.1, so it will ship in v3.7.4 unless gated first. No released stable version is affected.

Summary

/api/av/getAttributeViewFieldViews is registered with CheckAuth only and applies no publish-access filtering. Given a database avID, it returns the complete view structure of that database: every view's name, icon, layout type and the per-field Hidden flag, regardless of whether the caller is authorized to see that database or those views.

The route registered immediately before it, getAttributeViewKeys, does gate reader roles.

Details

| Item | Detail | |---|---| | Route | kernel/api/router.go:530 POST /api/av/getAttributeViewFieldViews → model.CheckAuth → getAttributeViewFieldViews | | Middleware | CheckAuth only, no CheckReadonly, no CheckAdminRole | | Guard in handler | None. Greps for IsReadOnlyRoleContext, publishAccess and Filter all return zero matches | | Exposed | For each entry in attrView.Views: ID, Icon, Name, Type, and the Hidden flag for the requested field | | Introduced by | acfc02ee8 (feature), dev branch only |

The leak path. The handler binds avID and keyID, validating them against the ID pattern only, then calls GetAttributeViewFieldViews(avID, keyID). That function calls av.ParseAttributeView(avID), which reads the database's storage file directly by identifier. There is no ownership check and no publish-access check anywhere on the path. The result is a per-view record for every view the database defines.

What a reader learns is author-written: view names are typed by the user, and layout type distinguishes a table from a gallery or kanban. The Hidden flag additionally reveals, per view, which fields the author chose to conceal.

Guarded sibling, one route earlier. getAttributeViewKeys (kernel/api/router.go:529) is also reader-reachable and does gate:

go if model.IsReadOnlyRoleContext(c) { blockAttributeViewKeys = model.FilterBlockAttributeViewKeysByPublishAccess(c, publishAccess, ...) }

The new route at 530 applies nothing equivalent.

This is worth flagging as more than an isolated omission. acfc02ee8 added a new reader-reachable attribute-view endpoint during the same development cycle in which the surrounding attribute-view endpoints were being hardened. The same pattern produced an earlier exposure: f2d966659, which added encrypted-notebook unlock status for plugin consumption, created a reader-reachable disclosure on a surface that was otherwise being tightened. A check on new reader-reachable routes against the gating applied to their registered neighbours would catch this class before release.

Reachability, stated honestly. The endpoint requires a valid avID and keyID. renderAttributeView supplies both to a reader for any database embedded in a document the reader can access. The realistic target is therefore a partially-published database: the document publishes one view, and this endpoint enumerates the existence, name, layout type and hidden-field configuration of the database's other views, including views the author did not publish. Against a database with no reader-accessible surface at all, obtaining the identifiers is harder, particularly since the getAttributeViewKeysByID enumeration path was closed on the development branch. The severity below reflects that constraint rather than a worst case.

Proof of Concept

Precondition: v3.7.4-alpha.1 build, publish mode enabled (default port 6808), anonymous when Publish.Auth.Enable is false, otherwise any publish reader account. A database embedded in a reader-accessible document, with at least one additional view that is not published.

Step 1, obtain the identifiers through the reader-accessible path:

POST http://127.0.0.1:6808/api/av/renderAttributeView {"id":"<block id of the embedded database in a published doc>"}

→ 200, returns the published view together with its key identifiers

Step 2, enumerate the rest of the database's views:

POST http://127.0.0.1:6808/api/av/getAttributeViewFieldViews {"avID":"<avID from step 1>","keyID":"<keyID from step 1>"}

→ 200, returns a record per view: ID, Icon, Name, Type, Hidden

The response includes views that were never published, disclosing their author-written names, their layout types, and which field is hidden in each.

Impact

An anonymous reader in publish mode, or any publish RoleReader, can enumerate the full view structure of a database when only part of that database is published. View names are author-written free text and frequently describe the data they filter, so this discloses the existence and subject matter of unpublished views. The per-field Hidden flags additionally reveal the author's concealment choices across those views. Confidentiality only, with no integrity or availability impact, and bounded by the reader needing identifiers obtainable from a reader-accessible database.

Suggested fix

Gate the handler for read-only roles, mirroring getAttributeViewKeys: when IsReadOnlyRoleContext(c) holds, resolve the block that owns the avID and apply the publish-access check before returning any view record, or filter the returned set through the same FilterBlockAttributeViewKeysByPublishAccess path its neighbour uses.

1 / 3
Source: OSV
First published (updated )
Severity
7.6
AV:A/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

SiYuan 3.6.0 before 3.8.7 accepts and persists the networkServeTLS setting but never enforces it, so the kernel port keeps serving full API access over plain HTTP alongside HTTPS. Network-adjacent attackers can sniff the cleartext login flow and session cookie, which lacks the Secure attribute while responses omit HSTS, to hijack kernel API sessions.

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

SiYuan from 2.10.0 before 3.8.7 contains a stored cross-site scripting vulnerability that allows users with edit permission to inject script by placing javascript: URLs in math block content rendered with KaTeX trust enabled. Attackers can store \href, \url, or \includegraphics payloads that mathRender assigns via innerHTML, executing script in the application origin when a reader clicks the rendered link.

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.5.4 before 3.8.7 on Windows contains a sensitive-path denylist bypass in IsSensitivePath because the Windows extended-length prefix (\\?\) survives filepath.Clean and matches no forbidden prefix. Authenticated API callers can prepend the prefix to localPath in /api/import/importStdMd to copy workspace conf files, including the API token and TLS private keys, into assets served to unauthenticated clients.

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

SiYuan before v3.8.7 contains an sql injection vulnerability that allows admin-token holders to read history.db by injecting unescaped op and type=3 query values. Attackers can send crafted UNION SELECT payloads to searchHistory or getHistoryItems endpoints to exfiltrate deleted document content, titles, and paths.

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

SiYuan before v3.8.4 contains an authorization omission in the siyuan-get IPC handler that allows remote-kernel renderers to access native clipboard formats by invoking clipboardReadMathML, clipboardReadOffice, and clipboardReadWPS commands with matching plaintext. Attackers controlling remote renderer content can obtain MathML formulas, Office bytes, and WPS bytes from local clipboard during user-mediated paste operations.

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

SiYuan before v3.8.4 contains an authentication bypass vulnerability in the publish service where session cookies are issued without Secure or SameSite attributes over plaintext HTTP connections. An on-path attacker can observe a valid publish-visitor-session-id cookie from a Basic Auth exchange and replay it to access authenticated publish endpoints without knowing the account password.

First published (updated )
Severity
7.6
XSS
AV:N/AC:H/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

SiYuan from 2.9.0 before 3.8.7 contains a cross-site scripting vulnerability in the unauthenticated /check-auth page that assigns the to parameter to window.location.href without scheme validation. Attackers can send a crafted link with a javascript: URI that executes after victim login, invoking kernel APIs to read or modify workspace content and settings.

First published (updated )
Severity
7.1
Path Traversal
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

SiYuan before v3.7.2 contains a path traversal vulnerability in the /export/temp/ short-circuit branch of the serveExport handler (kernel/server/serve.go). Unlike the main export branch, this branch joins the raw, percent-decoded request path with util.TempDir and serves the file without the IsSubPath or IsSensitivePath checks added in the earlier export-disclosure hardening (GHSA-6865-qjcf-286f). An authenticated attacker can send percent-encoded traversal sequences (e.g. /export/temp/%2e%2e/.../etc/passwd, where %2e%2e is decoded to '..') to read arbitrary files outside TempDir, including /etc/passwd, SSH keys (~/.ssh/), and SiYuan workspace .db and .log files, bypassing the sensitive-file protection.

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