siyuan-note/siyuan: SiYuan: \\?\GLOBALROOT alias bypasses IsSensitivePath (v3.8.7 fix incomplete) — API token + TLS keys copied into served assets
What fixes it
No source in this snapshot names a fixed version.
Description
Summary
The v3.8.7 fix for GHSA-6gmc added resolveNamespaceAlias() to normalize Windows namespace aliases (\\?\, \\.\, \\?\UNC\, local admin shares) before the sensitive-path denylist is evaluated. The normalization only strips the literal backslash spellings; the device-namespace alias \\?\GLOBALROOT\Device\HarddiskVolumeN\... (and \\.\GLOBALROOT\..., the forward-slash spelling, and HarddiskVolumeShadowCopyN) survives it. After stripping, the path becomes GLOBALROOT\Device\HarddiskVolumeN\..., which matches no denylist entry — neither in the first isSensitivePath() check nor in the ResolveLongestExistingParent() re-check in the IsSensitivePath() wrapper.
Windows accepts these paths at open time (verified: CreateFileW — the layer the kernel's Go code uses — opens and reads them; the volume index is enumerable by trial). Every endpoint that relies on util.IsSensitivePath as its only guard is therefore bypassable with a new spelling, with the same end-to-end chain as GHSA-6gmc: POST /api/import/importStdMd copies <workspace>\conf into data/assets/, which the kernel serves back; the workspace conf.json (containing the API token) and the TLS private key (ca-*.key) become readable.
Weakness
- CWE-41 (Improper Resolution of Path Equivalence) → CWE-22 (Path Traversal); secondary CWE-200 (exposure of the copied secrets).
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N — 7.5 (High). The vector mirrors the vendor's own scoring of GHSA-6gmc, whose chain this finding reproduces through a spelling the fix does not normalize.
Affected component
- Repository:
siyuan-note/siyuan(kernel) kernel/util/path.go:resolveNamespaceAlias()— strips only `\\?\and\\.\literally (strings.HasPrefix), thenfilepath.Clean; handlesUNC\andlocalhost/<drive>$admin shares; returns everything else unchanged. TheGLOBALROOT` device-namespace prefix is not handled.isSensitivePath()—p = resolveNamespaceAlias(p)then lowercasedHasPrefixchecks (workspaceconf/temp, home dotfiles incl..env,credentials/id_name prefixes,c:\windows\system32, …).IsSensitivePath()wrapper — firstisSensitivePath(p,false), then early-falseifIsSubPath(WorkspaceDir, resolveNamespaceAlias(p)), thenresolved := ResolveLongestExistingParent(p); ifresolved != p, re-checkisSensitivePath(resolved,false).- Affected versions: the fix shipped in v3.8.7 (still beta at the time of writing; verified on
v3.8.7-beta.3). The original advisory's range>= 3.5.4, <= 3.8.6should be extended to include the v3.8.7 line.
Root cause
The alias normalization is a literal prefix strip; the device-namespace form is a different prefix family:
// kernel/util/path.go — resolveNamespaceAlias() (v3.8.7) // strips only the literal spellings: // \\?\ \\.\ // then filepath.Clean
Why \\?\GLOBALROOT\Device\HarddiskVolume3\<workspace>\conf survives all three steps:
resolveNamespaceAlias(...)→ strips\\?\→ `GLOBALROOT\Device\HarddiskVolume3\<ws>\conf(Clean` leaves the relative-looking remainder).- First check: lowercased =
globalroot\device\harddiskvolume3\<ws>\conf— no denylist prefix matches (d:\<ws>\confetc.). - Wrapper:
IsSubPathfalse → no early exit;ResolveLongestExistingParentpreserves the volume spelling (the path exists) → re-check strips\\?\again →GLOBALROOT\...→ miss again →IsSensitivePathreturns false.
The forward-slash spelling (//?/GLOBALROOT/...) additionally fails the literal strip and Clean canonicalizes it to \\?\GLOBALROOT\... — same miss.
Reproduction (dynamic — real Windows 10, SiYuan v3.8.7-beta.3)
Test setup: SiYuan v3.8.7-beta.3 installed; kernel run in server mode with a fresh workspace (SiYuan-Kernel.exe serve --workspace=<ws> --port=6808); API token read from the fresh workspace conf/conf.json; a notebook created via POST /api/notebook/createNotebook.
A. Windows accepts the alias at the layer the kernel uses (python → CreateFileW; the kernel is Go, which uses the same layer):
os.path.exists('\\?\GLOBALROOT\Device\HarddiskVolume3\Windows\win.ini') → True
os.stat(...).st_size → 92
open(...).read(50) → '; for 16-bit app support\n[fonts]\n...'(HarddiskVolume3 = C:, found by enumerating 1..6. The cmd/.NET layers reject the spelling at their own levels — irrelevant to the kernel's Go code.)
B. Control — the plain workspace conf path is refused by the fix:
POST /api/import/importStdMd {"notebook":"<id>","toPath":"/","localPath":"C:\Users\<user>\...\ws\conf"}
→ {"code":-1,"msg":"import from local path [C:\\Users\\...\\ws\\conf] failed: local path is sensitive path","data":null}C. Bypass — the same call with the GLOBALROOT alias:
POST /api/import/importStdMd {"notebook":"<id>","toPath":"/","localPath":"\\?\GLOBALROOT\Device\HarddiskVolume3\Users\<user>\...\ws\conf"}
→ {"code":0,"msg":"","data":null}D. Result — the sensitive files landed in the publicly served assets directory:
data/assets/: conf-*.json (the workspace conf; API token byte-identical to the source conf — verified MATCH True)
cert-*.pem (-----BEGIN CERTIFICATE-----)
ca-*.key (-----BEGIN EC PRIVATE KEY-----)
ar-*.json (i18n data, incidental)
GET /assets/conf-*.json → HTTP 200 (served back)The API token grants full control of the SiYuan instance; the TLS private key (with its cert) allows impersonation of the instance's HTTPS endpoint.
Impact
- Confidentiality — workspace secrets exfiltrated (C:H). The API token and the TLS private key are copied into
data/assets/, which the kernel serves over HTTP without authentication; any party able to reach the instance (or any caller allowed to invoke the import API) can read them. The same form reaches~/.ssh,~/.env,~/AppData, etc. on Windows. - The identical chain was rated 7.5 High in GHSA-6gmc; this finding is the same primitive through a spelling the fix does not normalize, so the fix's protection does not hold against it.
- Availability/integrity of the component itself are unaffected (A:N/I:N).
Suggested fix
Extend resolveNamespaceAlias to resolve GLOBALROOT forms to the underlying volume path (map Device\HarddiskVolumeN / HarddiskVolumeShadowCopyN to the mounted drive letters via QueryDosDevice / GetVolumeNameForVolumeMountPoint, then re-run the denylist), or reject GLOBALROOT/device-namespace prefixes outright at the API/MCP boundary (they are never legitimate for user-supplied file paths).
Regression cases: \\?\GLOBALROOT\..., \\.\GLOBALROOT\..., //?/GLOBALROOT/..., lowercase harddiskvolumen, HarddiskVolumeShadowCopyN, and GLOBALROOT forms of each denied prefix.
References
GHSA-6gmc-424x-xgpm(7.5, published 2026-10-09) — the original\\?\denylist bypass; fix commits98487d43dband9ffcc73423, shipped in v3.8.7.- Repository: https://github.com/siyuan-note/siyuan — verified against v3.8.7-beta.3 on Windows 10 22H2.
Notes
- Verified end-to-end on a real Windows host (the fix's target platform); the test used a fresh workspace and all temporary artifacts were removed afterwards.
- The volume index (
HarddiskVolumeN) varies per machine and is enumerable by trial (single-byte enumeration cost); the attacker needs no knowledge of the mapping in advance. - No attribution; nothing sent.
Text from GitHub Advisory Database
Affected versions 1
- siyuan-note/siyuan, go>= 3.5.4GitHub