GHSA-3CM4-CCVW-6XR6
Vulnerability from github – Published: 2026-10-05 17:32 – Updated: 2026-10-05 17:32Summary
GHSA-c8r8-95hg-mp34 added a centralized guard,
util.IsForbiddenAbsPath(), specifically to block access to a small
set of sensitive files: conf/conf.json (plaintext
accessAuthCode/API token/cookie key), data/snippets/conf.json,
the entire data/templates/ directory, and
data/.siyuan/publishAccess.json (plaintext publish-mode passwords).
It was applied to kernel/api/file.go and kernel/mcp/tools/file.go.
Two other routes in the same server that serve arbitrary files by path,
/history/*path and /repo/diff/*path, construct their target paths
independently and were not updated to call this new guard. Since the
repo/history snapshot system's tracked root is data/ (confirmed by
getSyncIgnoreLines(), whose ignore file lives at
data/.siyuan/syncignore with entries relative to data/), both
data/.siyuan/publishAccess.json and data/templates/* fall within
the scope that can legitimately be captured in historical snapshots,
meaning a prior version of either file can exist in
util.HistoryDir/the repo-diff temp checkout even after the live file
has been protected by the new guard. This is CWE-862 (Missing
Authorization) applied to a very recently introduced protection
mechanism.
Details
kernel/server/serve.go, /history/*path (around line 994):
ginServer.GET("/history/*path", model.CheckAuth, model.CheckAdminRole, func(context *gin.Context) {
p := filepath.Join(util.HistoryDir, context.Param("path"))
// 加密笔记本的历史是密文(.sy/assets/AV),需先解密再输出
if serveEncryptedHistory(context, p) {
return
}
secureAssetContentHeaders(context, p, p)
http.ServeFile(context.Writer, context.Request, p)
})
No call to util.IsForbiddenAbsPath(p) anywhere in this handler.
kernel/server/serve.go, /repo/diff/*path (around line 1241):
ginServer.GET("/repo/diff/*path", model.CheckAuth, model.CheckAdminRole, func(context *gin.Context) {
requestPath := filepath.Clean(context.Param("path"))
if strings.Contains(requestPath, "..") {
context.Status(http.StatusUnauthorized)
return
}
...
p := filepath.Join(repoDiffBaseDir, requestPath)
if !gulu.File.IsSubPath(repoDiffBaseDir, p) {
context.Status(http.StatusUnauthorized)
return
}
http.ServeFile(context.Writer, context.Request, p)
})
This route does have its own traversal protection (.. rejection and
IsSubPath containment within repoDiffBaseDir), but that only
prevents escaping the diff-checkout directory, it does nothing to
prevent retrieving a legitimately checked-out historical copy of
publishAccess.json or a templates file from within that directory,
which is exactly what the new guard exists to prevent regardless of
which directory the copy currently sits in.
util.IsForbiddenAbsPath() itself (kernel/util/path_guard.go,
introduced by the referenced fix) confirms the intended scope:
// 禁止访问 data/.siyuan/publishAccess.json(含发布模式明文访问密码)
publishAccessPath := NormalizeAndResolve(filepath.Join(DataDir, ".siyuan", "publishAccess.json"))
if fileNorm == publishAccessPath {
return true
}
and
// 禁止访问 data/templates 目录(含目录本身及其全部子路径)
templatesBase := NormalizeAndResolve(filepath.Join(DataDir, "templates"))
if fileNorm == templatesBase || gulu.File.IsSubPath(templatesBase, fileNorm) {
return true
}
Both are paths within data/, the same root the sync/history/repo
system tracks.
Step-by-step reproduction
- As the workspace admin, enable Publish with a password on at least
one notebook (creating
data/.siyuan/publishAccess.jsonwith a plaintext password), then let a sync/backup snapshot capture this state (or check whether local history capture already coversdata/.siyuan/in the deployed version). - Change or remove the publish password, so the live
publishAccess.jsonno longer contains the old plaintext password the new guard is meant to hide, going forward. - As the admin, request the historical/diff version instead of the
live file:
bash curl -s http://<target>:6806/history/<snapshot-path-to-publishAccess.json> \ -u "<workspaceName>:<accessAuthCode>" curl -s http://<target>:6806/repo/diff/<diff-path-to-publishAccess.json> \ -u "<workspaceName>:<accessAuthCode>" - Expected if consistently protected, matching the behavior the new
guard already provides on the live-file endpoints: rejected.
Observed: neither handler calls
IsForbiddenAbsPath, so the historical copy is served if it exists in that location.
(Not run against a live compiled kernel, same sandbox limitation noted
throughout this review; both handlers are read directly from source at
the reviewed commit, and IsForbiddenAbsPath's scope, plus the
sync-root confirmation via getSyncIgnoreLines(), are quoted directly
above. Whether these specific files are captured by history/repo
snapshots in a given deployment depends on the workspace's actual
usage history and was not independently verified against a live
instance in this review.)
Impact
An admin-authenticated request to either route can potentially retrieve
a historical copy of data/.siyuan/publishAccess.json (disclosing a
plaintext publish-mode password even after it has been changed or the
live file has been protected) or a data/templates/* file, directly
undermining the protection GHSA-c8r8-95hg-mp34 was written four days
prior to this review specifically to provide, via two routes that
predate that fix and were not updated alongside it.
```
Affected products
| Field | Value |
|---|---|
| Ecosystem | Go |
| Package name | github.com/siyuan-note/siyuan/kernel |
| Affected versions | Present as of commit 251596f (2026-08-12, the version this review confirmed), i.e. postdates and was not covered by the GHSA-c8r8-95hg-mp34 fix (commit 3542530, 2026-08-08) |
| Patched versions | (none yet, leave blank until a fix is released) |
Severity
| Field | Value |
|---|---|
| Vector string | CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:M/I:N/A:N |
| Score | ~5.9 (Medium), PR:H since admin authentication is required at the HTTP layer, confidentiality impact scoped to whatever sensitive historical content happens to exist in the tracked snapshots for a given deployment (a real but deployment-dependent condition, honestly reflected as Medium rather than assumed to always be present), no integrity/availability impact since both are read-only. |
Weaknesses (CWE)
- CWE-862: Missing Authorization (primary)
- CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
Notes for filing
- Direct, narrow follow-up to
GHSA-c8r8-95hg-mp34; recommend referencing that advisory directly when filing, since this is precisely the "sibling caller missed" pattern that fix's own centralization (moving the check into a sharedutilfunction) was presumably intended to prevent, just for two callers that existed before the shared function did and weren't migrated to it. - Suggested fix: add
if util.IsForbiddenAbsPath(p) { ... reject ... }to both handlers, matching the pattern already applied inkernel/api/file.goandkernel/mcp/tools/file.go.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/siyuan-note/siyuan/kernel"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.0.0-20260816034002-035bf9a8c311"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-200",
"CWE-862"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-05T17:32:44Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\n`GHSA-c8r8-95hg-mp34` added a centralized guard,\n`util.IsForbiddenAbsPath()`, specifically to block access to a small\nset of sensitive files: `conf/conf.json` (plaintext\n`accessAuthCode`/API token/cookie key), `data/snippets/conf.json`,\nthe entire `data/templates/` directory, and\n`data/.siyuan/publishAccess.json` (plaintext publish-mode passwords).\nIt was applied to `kernel/api/file.go` and `kernel/mcp/tools/file.go`.\nTwo other routes in the same server that serve arbitrary files by path,\n`/history/*path` and `/repo/diff/*path`, construct their target paths\nindependently and were not updated to call this new guard. Since the\nrepo/history snapshot system\u0027s tracked root is `data/` (confirmed by\n`getSyncIgnoreLines()`, whose ignore file lives at\n`data/.siyuan/syncignore` with entries relative to `data/`), both\n`data/.siyuan/publishAccess.json` and `data/templates/*` fall within\nthe scope that can legitimately be captured in historical snapshots,\nmeaning a prior version of either file can exist in\n`util.HistoryDir`/the repo-diff temp checkout even after the live file\nhas been protected by the new guard. This is CWE-862 (Missing\nAuthorization) applied to a very recently introduced protection\nmechanism.\n\n### Details\n`kernel/server/serve.go`, `/history/*path` (around line 994):\n```go\nginServer.GET(\"/history/*path\", model.CheckAuth, model.CheckAdminRole, func(context *gin.Context) {\n p := filepath.Join(util.HistoryDir, context.Param(\"path\"))\n // \u52a0\u5bc6\u7b14\u8bb0\u672c\u7684\u5386\u53f2\u662f\u5bc6\u6587\uff08.sy/assets/AV\uff09\uff0c\u9700\u5148\u89e3\u5bc6\u518d\u8f93\u51fa\n if serveEncryptedHistory(context, p) {\n return\n }\n secureAssetContentHeaders(context, p, p)\n http.ServeFile(context.Writer, context.Request, p)\n})\n```\nNo call to `util.IsForbiddenAbsPath(p)` anywhere in this handler.\n\n`kernel/server/serve.go`, `/repo/diff/*path` (around line 1241):\n```go\nginServer.GET(\"/repo/diff/*path\", model.CheckAuth, model.CheckAdminRole, func(context *gin.Context) {\n requestPath := filepath.Clean(context.Param(\"path\"))\n if strings.Contains(requestPath, \"..\") {\n context.Status(http.StatusUnauthorized)\n return\n }\n ...\n p := filepath.Join(repoDiffBaseDir, requestPath)\n if !gulu.File.IsSubPath(repoDiffBaseDir, p) {\n context.Status(http.StatusUnauthorized)\n return\n }\n http.ServeFile(context.Writer, context.Request, p)\n})\n```\nThis route does have its own traversal protection (`..` rejection and\n`IsSubPath` containment within `repoDiffBaseDir`), but that only\nprevents escaping the diff-checkout directory, it does nothing to\nprevent retrieving a *legitimately checked-out historical copy* of\n`publishAccess.json` or a templates file from within that directory,\nwhich is exactly what the new guard exists to prevent regardless of\nwhich directory the copy currently sits in.\n\n`util.IsForbiddenAbsPath()` itself (`kernel/util/path_guard.go`,\nintroduced by the referenced fix) confirms the intended scope:\n```go\n// \u7981\u6b62\u8bbf\u95ee data/.siyuan/publishAccess.json\uff08\u542b\u53d1\u5e03\u6a21\u5f0f\u660e\u6587\u8bbf\u95ee\u5bc6\u7801\uff09\npublishAccessPath := NormalizeAndResolve(filepath.Join(DataDir, \".siyuan\", \"publishAccess.json\"))\nif fileNorm == publishAccessPath {\n return true\n}\n```\nand\n```go\n// \u7981\u6b62\u8bbf\u95ee data/templates \u76ee\u5f55\uff08\u542b\u76ee\u5f55\u672c\u8eab\u53ca\u5176\u5168\u90e8\u5b50\u8def\u5f84\uff09\ntemplatesBase := NormalizeAndResolve(filepath.Join(DataDir, \"templates\"))\nif fileNorm == templatesBase || gulu.File.IsSubPath(templatesBase, fileNorm) {\n return true\n}\n```\nBoth are paths within `data/`, the same root the sync/history/repo\nsystem tracks.\n\n### Step-by-step reproduction\n1. As the workspace admin, enable Publish with a password on at least\n one notebook (creating `data/.siyuan/publishAccess.json` with a\n plaintext password), then let a sync/backup snapshot capture this\n state (or check whether local history capture already covers\n `data/.siyuan/` in the deployed version).\n2. Change or remove the publish password, so the live\n `publishAccess.json` no longer contains the old plaintext password\n the new guard is meant to hide, going forward.\n3. As the admin, request the historical/diff version instead of the\n live file:\n ```bash\n curl -s http://\u003ctarget\u003e:6806/history/\u003csnapshot-path-to-publishAccess.json\u003e \\\n -u \"\u003cworkspaceName\u003e:\u003caccessAuthCode\u003e\"\n curl -s http://\u003ctarget\u003e:6806/repo/diff/\u003cdiff-path-to-publishAccess.json\u003e \\\n -u \"\u003cworkspaceName\u003e:\u003caccessAuthCode\u003e\"\n ```\n4. Expected if consistently protected, matching the behavior the new\n guard already provides on the live-file endpoints: rejected.\n Observed: neither handler calls `IsForbiddenAbsPath`, so the\n historical copy is served if it exists in that location.\n\n*(Not run against a live compiled kernel, same sandbox limitation noted\nthroughout this review; both handlers are read directly from source at\nthe reviewed commit, and `IsForbiddenAbsPath`\u0027s scope, plus the\nsync-root confirmation via `getSyncIgnoreLines()`, are quoted directly\nabove. Whether these specific files are captured by history/repo\nsnapshots in a given deployment depends on the workspace\u0027s actual\nusage history and was not independently verified against a live\ninstance in this review.)*\n\n### Impact\nAn admin-authenticated request to either route can potentially retrieve\na historical copy of `data/.siyuan/publishAccess.json` (disclosing a\nplaintext publish-mode password even after it has been changed or the\nlive file has been protected) or a `data/templates/*` file, directly\nundermining the protection `GHSA-c8r8-95hg-mp34` was written four days\nprior to this review specifically to provide, via two routes that\npredate that fix and were not updated alongside it.\n```\n\n## Affected products\n\n| Field | Value |\n|---|---|\n| Ecosystem | **Go** |\n| Package name | `github.com/siyuan-note/siyuan/kernel` |\n| Affected versions | Present as of commit `251596f` (2026-08-12, the version this review confirmed), i.e. postdates and was not covered by the `GHSA-c8r8-95hg-mp34` fix (commit `3542530`, 2026-08-08) |\n| Patched versions | *(none yet, leave blank until a fix is released)* |\n\n## Severity\n\n| Field | Value |\n|---|---|\n| Vector string | `CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:M/I:N/A:N` |\n| Score | **~5.9 (Medium)**, `PR:H` since admin authentication is required at the HTTP layer, confidentiality impact scoped to whatever sensitive historical content happens to exist in the tracked snapshots for a given deployment (a real but deployment-dependent condition, honestly reflected as Medium rather than assumed to always be present), no integrity/availability impact since both are read-only. |\n\n## Weaknesses (CWE)\n\n- **CWE-862**: Missing Authorization (primary)\n- **CWE-200**: Exposure of Sensitive Information to an Unauthorized Actor\n\n## Notes for filing\n- Direct, narrow follow-up to `GHSA-c8r8-95hg-mp34`; recommend\n referencing that advisory directly when filing, since this is\n precisely the \"sibling caller missed\" pattern that fix\u0027s own\n centralization (moving the check into a shared `util` function) was\n presumably intended to prevent, just for two callers that existed\n before the shared function did and weren\u0027t migrated to it.\n- Suggested fix: add `if util.IsForbiddenAbsPath(p) { ... reject ... }`\n to both handlers, matching the pattern already applied in\n `kernel/api/file.go` and `kernel/mcp/tools/file.go`.",
"id": "GHSA-3cm4-ccvw-6xr6",
"modified": "2026-10-05T17:32:44Z",
"published": "2026-10-05T17:32:44Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-3cm4-ccvw-6xr6"
},
{
"type": "WEB",
"url": "https://github.com/siyuan-note/siyuan/commit/035bf9a8c311c6f8468f2f1054b4d5e1711787e2"
},
{
"type": "PACKAGE",
"url": "https://github.com/siyuan-note/siyuan"
},
{
"type": "WEB",
"url": "https://github.com/siyuan-note/siyuan/releases/tag/v3.8.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "SiYuan: /history/*path and /repo/diff/*path potentially exposing historical snapshots of data/.siyuan/publishAccess.json and data/templates/*"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.