CVE-2026-62204
SiYuan before v3.7.4 Plugin Overwrite via Bazaar Install
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.
Attack profile
How easy it is to exploit
- Attack vector
- Local
- Complexity
- High
- Privileges needed
- Low
- User interaction
- Passive
What an attacker can affect
- Confidentiality
- Low
- Integrity
- High
- Availability
- Low
Exploit likelihood
What each source reports
CVE List Score used
5.9
SiYuan before v3.7.4 Plugin Overwrite via Bazaar Install
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.
- Record
CVE-2026-62204- CVSS 4.0
- 5.9 Medium
CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:P/VC:L/VI:H/VA:L/SC:L/SI:H/SA:L - CVSS 3.1
- 6.6 Medium
CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:C/C:L/I:H/A:L - Weakness
- CWE-345
- Updated
- 2026-11-02
EUVDEUVD-2026-64270
5.9
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.
- Record
EUVD-2026-64270- CVSS 4.0
- 5.9 Medium
CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:P/VC:L/VI:H/VA:L/SC:L/SI:H/SA:L - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-989h-w3q9-3m2f
5.9
Duplicate Advisory: SiYuan: Missing consistency check between packageName and repoURL/repoHash in bazaar package install allows overwriting an existing, trusted plugin
Duplicate Advisory
This advisory has been withdrawn because it is a duplicate of GHSA-rpx2-p6hp-x5gj. This link is maintained to preserve external references.
Original Description
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.
- Record
GHSA-989h-w3q9-3m2f- CVSS 4.0
- 5.9 Medium
CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:P/VC:L/VI:H/VA:L/SC:L/SI:H/SA:L/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 - CVSS 3.1
- 6.6 Medium
CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:C/C:L/I:H/A:L - Weakness
- CWE-345
- Updated
- 2026-11-02
GitHub AdvisoriesGHSA-rpx2-p6hp-x5gj
6.6
SiYuan: Missing consistency check between packageName and repoURL/repoHash in bazaar package install allows overwriting an existing, trusted plugin
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:
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:
func getPackageInstallPath(pkgType, packageName string) (string, string, error) {
case "plugins":
return filepath.Join(util.DataDir, "plugins", packageName), "plugin.json", nil
kernel/bazaar/install.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:
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:
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…
- Record
GHSA-rpx2-p6hp-x5gj- CVSS 3.1
- 6.6 Medium
CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:C/C:L/I:H/A:L - Weakness
- CWE-345, CWE-668
- Updated
- 2026-11-02
NVD
5.9
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.
- Record
CVE-2026-62204- CVSS 4.0
- 5.9 Medium
CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:P/VC:L/VI:H/VA:L/SC:L/SI:H/SA:L/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 - CVSS 3.1
- 6.6 Medium
CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:C/C:L/I:H/A:L - Weakness
- CWE-345
- Updated
- 2026-11-02
OSVGHSA-989h-w3q9-3m2f
5.9
Duplicate Advisory: SiYuan: Missing consistency check between packageName and repoURL/repoHash in bazaar package install allows overwriting an existing, trusted plugin
Duplicate Advisory
This advisory has been withdrawn because it is a duplicate of GHSA-rpx2-p6hp-x5gj. This link is maintained to preserve external references.
Original Description
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.
- Record
GHSA-989h-w3q9-3m2f- CVSS 4.0
- 5.9 Medium
CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:P/VC:L/VI:H/VA:L/SC:L/SI:H/SA:L/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 - CVSS 3.1
- 6.6 Medium
CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:C/C:L/I:H/A:L - Weakness
- CWE-345
- Updated
- 2026-11-02
OSVGHSA-rpx2-p6hp-x5gj
6.6
SiYuan: Missing consistency check between packageName and repoURL/repoHash in bazaar package install allows overwriting an existing, trusted plugin
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:
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:
func getPackageInstallPath(pkgType, packageName string) (string, string, error) {
case "plugins":
return filepath.Join(util.DataDir, "plugins", packageName), "plugin.json", nil
kernel/bazaar/install.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:
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:
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…
- Record
GHSA-rpx2-p6hp-x5gj- CVSS 3.1
- 6.6 Medium
CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:C/C:L/I:H/A:L - Weakness
- CWE-345, CWE-668
- Updated
- 2026-11-06
OSVGO-2026-6834 No score
SiYuan: Missing consistency check between packageName and repoURL/repoHash in bazaar package install allows overwriting an existing, trusted plugin in github.com/siyuan-note/siyuan/kernel
SiYuan: Missing consistency check between packageName and repoURL/repoHash in bazaar package install allows overwriting an existing, trusted plugin in github.com/siyuan-note/siyuan/kernel
- Record
GO-2026-6834- Updated
- 2026-11-06
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
github.com/siyuan-note/siyuan/kernelGo |
before 0.0.0-20260808010544-26ccb40d390e | 0.0.0-20260808010544-26ccb40d390e |
History
| Recorded | Changed | Sources |
|---|---|---|
| affected, aliases, facts, references | OSV | |
| affected | ||
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-62204 is a medium vulnerability (CVSS 5.9) in Siyuan-note Siyuan, published August 22, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.09%, the 0th percentile. Fixed in github.com/siyuan-note/siyuan/kernel 0.0.0-20260808010544-26ccb40d390e.
References
github.com/siyuan-note/siyuan/security/advisories/GHSA-rpx2-p6hp-x5gj
vulncheck.com/advisories/siyuan-before-plugin-overwrite-via-bazaar-install
nvd.nist.gov/vuln/detail/CVE-2026-62204
github.com/advisories/GHSA-989h-w3q9-3m2f
github.com/siyuan-note/siyuan/commit/26ccb40d390eb7d9b04673629a58ab1d69cc7e53
github.com/siyuan-note/siyuan/releases/tag/v3.8.0
github.com/advisories/GHSA-rpx2-p6hp-x5gj
github.com/siyuan-note/siyuan
Left out here: the rest of 2 source descriptions. The JSON record has them.