{"schema":"threatline.record/2","record":{"id":"GHSA-gg3p-r4g8-vxg2","aliases":["GHSA-gg3p-r4g8-vxg2"],"summary":"SiYuan: \\\\?\\GLOBALROOT alias bypasses IsSensitivePath (v3.8.7 fix incomplete) — API token + TLS keys copied into served assets","details":"## Summary\r\n\r\nThe 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.\r\n\r\nWindows 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.\r\n\r\n## Weakness\r\n\r\n- CWE-41 (Improper Resolution of Path Equivalence) → CWE-22 (Path Traversal); secondary CWE-200 (exposure of the copied secrets).\r\n- 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.\r\n\r\n## Affected component\r\n\r\n- Repository: `siyuan-note/siyuan` (kernel)\r\n- `kernel/util/path.go`:\r\n  - `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.\r\n  - `isSensitivePath()` — `p = resolveNamespaceAlias(p)` then lowercased `HasPrefix` checks (workspace `conf`/`temp`, home dotfiles incl. `.env`, `credentials`/`id_` name prefixes, `c:\\windows\\system32`, …).\r\n  - `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)`.\r\n- 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.\r\n\r\n## Root cause\r\n\r\nThe alias normalization is a literal prefix strip; the device-namespace form is a different prefix family:\r\n\r\n```go\r\n// kernel/util/path.go — resolveNamespaceAlias() (v3.8.7)\r\n// strips only the literal spellings:\r\n//   \\\\?\\   \\\\.\\\r\n// then filepath.Clean\r\n```\r\n\r\nWhy `\\\\?\\GLOBALROOT\\Device\\HarddiskVolume3\\<workspace>\\conf` survives all three steps:\r\n\r\n1. `resolveNamespaceAlias(...)` → strips `\\\\?\\` → `` `GLOBALROOT\\Device\\HarddiskVolume3\\<ws>\\conf` `` (`Clean` leaves the relative-looking remainder).\r\n2. First check: lowercased = `globalroot\\device\\harddiskvolume3\\<ws>\\conf` — no denylist prefix matches (`d:\\<ws>\\conf` etc.).\r\n3. Wrapper: `IsSubPath` false → no early exit; `ResolveLongestExistingParent` preserves the volume spelling (the path exists) → re-check strips `\\\\?\\` again → `GLOBALROOT\\...` → miss again → `IsSensitivePath` returns **false**.\r\n\r\nThe forward-slash spelling (`//?/GLOBALROOT/...`) additionally fails the literal strip and `Clean` canonicalizes it to `\\\\?\\GLOBALROOT\\...` — same miss.\r\n\r\n## Reproduction (dynamic — real Windows 10, SiYuan v3.8.7-beta.3)\r\n\r\nTest 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`.\r\n\r\n**A. Windows accepts the alias at the layer the kernel uses** (`python` → `CreateFileW`; the kernel is Go, which uses the same layer):\r\n\r\n```\r\nos.path.exists('\\\\?\\GLOBALROOT\\Device\\HarddiskVolume3\\Windows\\win.ini')  → True\r\nos.stat(...).st_size                                                     → 92\r\nopen(...).read(50)                                                       → '; for 16-bit app support\\n[fonts]\\n...'\r\n```\r\n\r\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.)\r\n\r\n**B. Control — the plain workspace conf path is refused by the fix:**\r\n\r\n```\r\nPOST /api/import/importStdMd  {\"notebook\":\"<id>\",\"toPath\":\"/\",\"localPath\":\"C:\\Users\\<user>\\...\\ws\\conf\"}\r\n→ {\"code\":-1,\"msg\":\"import from local path [C:\\\\Users\\\\...\\\\ws\\\\conf] failed: local path is sensitive path\",\"data\":null}\r\n```\r\n\r\n**C. Bypass — the same call with the GLOBALROOT alias:**\r\n\r\n```\r\nPOST /api/import/importStdMd  {\"notebook\":\"<id>\",\"toPath\":\"/\",\"localPath\":\"\\\\?\\GLOBALROOT\\Device\\HarddiskVolume3\\Users\\<user>\\...\\ws\\conf\"}\r\n→ {\"code\":0,\"msg\":\"\",\"data\":null}\r\n```\r\n\r\n**D. Result — the sensitive files landed in the publicly served assets directory:**\r\n\r\n```\r\ndata/assets/: conf-*.json  (the workspace conf; API token byte-identical to the source conf — verified MATCH True)\r\n             cert-*.pem   (-----BEGIN CERTIFICATE-----)\r\n             ca-*.key     (-----BEGIN EC PRIVATE KEY-----)\r\n             ar-*.json    (i18n data, incidental)\r\nGET /assets/conf-*.json → HTTP 200 (served back)\r\n```\r\n\r\nThe API token grants full control of the SiYuan instance; the TLS private key (with its cert) allows impersonation of the instance's HTTPS endpoint.\r\n\r\n## Impact\r\n\r\n- **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.\r\n- 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.\r\n- Availability/integrity of the component itself are unaffected (A:N/I:N).\r\n\r\n## Suggested fix\r\n\r\nExtend `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).\r\n\r\nRegression cases: `\\\\?\\GLOBALROOT\\...`, `\\\\.\\GLOBALROOT\\...`, `//?/GLOBALROOT/...`, lowercase `harddiskvolumen`, `HarddiskVolumeShadowCopyN`, and GLOBALROOT forms of each denied prefix.\r\n\r\n## References\r\n\r\n- `GHSA-6gmc-424x-xgpm` (7.5, published 2026-10-09) — the original `\\\\?\\` denylist bypass; fix commits `98487d43db` and `9ffcc73423`, shipped in v3.8.7.\r\n- Repository: https://github.com/siyuan-note/siyuan — verified against v3.8.7-beta.3 on Windows 10 22H2.\r\n\r\n## Notes\r\n\r\n- 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.\r\n- 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.\r\n- No attribution; nothing sent.\r\n","categories":["Unclassified"],"product":"siyuan-note/siyuan","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/siyuan-note/siyuan/security/advisories/GHSA-gg3p-r4g8-vxg2","source_modified_at":"2026-10-13T05:22:48Z","fetched_at":"2026-10-28T20:58:01.013507Z","package":{"ecosystem":"go","name":"siyuan-note/siyuan"},"range":">= 3.5.4"}],"fixed_versions":[],"cvss":7.5,"severity":"High","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-13T05:22:48Z","modified_at":"2026-10-13T05:22:48Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/siyuan-note/siyuan/security/advisories/GHSA-gg3p-r4g8-vxg2","fetched_at":"2026-10-28T20:58:01.013507Z","source_modified_at":"2026-10-13T05:22:48Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-13T05:22:48Z","content_hash":"946bd29d1132e4a8fcd1f02243e50996a466fcd86d7fec2dc70c0a239b0a6ff2"}}
