All vulnerabilities
GHSA-gg3p-r4g8-vxg2

siyuan-note/siyuan: SiYuan: \\?\GLOBALROOT alias bypasses IsSensitivePath (v3.8.7 fix incomplete) — API token + TLS keys copied into served assets

siyuan-note/siyuan
CVSS
7.5High
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed inNone listedNo source names a fixed version yet
Published13 Oct 2026Updated 13 Oct 2026GitHub

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), then filepath.Clean; handles UNC\ and localhost/<drive>$ admin shares; returns everything else unchanged. The GLOBALROOT` device-namespace prefix is not handled.
  • isSensitivePath() — p = resolveNamespaceAlias(p) then lowercased HasPrefix checks (workspace conf/temp, home dotfiles incl. .env, credentials/id_ name prefixes, c:\windows\system32, …).
  • IsSensitivePath() wrapper — first isSensitivePath(p,false), then early-false if IsSubPath(WorkspaceDir, resolveNamespaceAlias(p)), then resolved := ResolveLongestExistingParent(p); if resolved != p, re-check isSensitivePath(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.6 should 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:

  1. resolveNamespaceAlias(...) → strips \\?\ → ` GLOBALROOT\Device\HarddiskVolume3\<ws>\conf (Clean` leaves the relative-looking remainder).
  2. First check: lowercased = globalroot\device\harddiskvolume3\<ws>\conf — no denylist prefix matches (d:\<ws>\conf etc.).
  3. Wrapper: IsSubPath false → no early exit; ResolveLongestExistingParent preserves the volume spelling (the path exists) → re-check strips \\?\ again → GLOBALROOT\... → miss again → IsSensitivePath returns 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 commits 98487d43db and 9ffcc73423, 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