Skip to content

Incomplete fix of GHSA-p23f-cm6q-2qp8: SiYuan's sensitive-path guard misses `.env`, so the agent-facing MCP asset tool uploads it into the workspace where the server then serves it back

High
88250 published GHSA-q768-4j87-gf3w Oct 9, 2026

Package

gomod siyuan-note (Go)

Affected versions

<= 3.8.6

Patched versions

v3.8.7

Description

Class: path traversal (local file read into workspace, CWE-22 / CWE-200)
Execution: executed
Location: kernel/mcp/tools/asset.go AssetTool.upload -> validateAssetUploadPaths -> util.IsSensitivePath (kernel/util/path.go:485 black name list), fix commit b26a4a3 (parent advisory GHSA-p23f-cm6q-2qp8)
Severity: High

Details

The fix for GHSA-p23f-cm6q-2qp8 introduced validateAssetUploadPaths in kernel/mcp/tools/asset.go, which passes every files argument of the MCP asset tool through filepath.Abs and then util.IsSensitivePath (kernel/util/path.go:443), rejecting paths that resolve into a sensitive prefix. That guard is a blacklist: isSensitivePath (kernel/util/path.go:485-612) walks sysPaths (system directories), Windows system prefixes, workspace-internal conf/temp, a home dotfile whitelist of 17 entries (.ssh, .aws, .kube, .docker, .gnupg, .git-credentials, ...) and names starting with credentials / id_.

Two blacklist gaps make the fix incomplete:

  1. .env is not on the home dotfile list. The de-facto standard location for application secrets on a single-user dev/staging host is ~/.env (and .env.local etc. — any filename not on the 17-entry list). A deployment whose backend reads dotenv-style configuration from the user's home directory stores a credential file the guard's own author clearly intended to block, at a path the guard accepts.
  2. The list also misses ~/Library/** (macOS Keychain), other users' home directories (/home/<otheruser>/) and Windows %APPDATA%/%LOCALAPPDATA% profile data — the same one-line-per-item omission class; .env is the finding we executed.

After validateAssetUploadPaths accepts the path, asset upload continues into InsertLocalAssets (kernel/model/upload.go:178), which copies each listed local file into the workspace data/assets/ directory under a generated asset name, then returns the workspace-relative asset path to the caller. Files inside the workspace are then served back over HTTP (GET /assets/<name> for authenticated sessions). The copy-in step turns "may upload a local file into the workspace" into "may read a local file back out of the workspace over HTTP" — a local-file-read oracle reachable entirely from the agent-facing MCP surface, with no filesystem access required of the attacker.

PoC

Executed against a self-hosted SiYuan v3.8.6 lab (Docker, b3log/siyuan), API token of an agent/admin principal used to drive the MCP server:

  1. Planted a secret-looking file outside the workspace:
    echo "AWS_SECRET_ACCESS_KEY=POC-FAKE-SECRET-1234" > /home/siyuan/.env (inside the SiYuan container, the server's own user home).

  2. Created a real document and called the fixed asset tool:

    POST /mcp (auth: Authorization: Bearer <Conf.Api.Token>):
    tools/call -> {"name":"asset","arguments":{"action":"upload","id":"20261004215722-wyx7tf4","files":"/home/siyuan/.env"}}

    Response:

    Uploaded 1 file(s):
    - .env -> assets/asset-20261004215731-mksvz7f.env
    
  3. Read back through the server's own static asset route:
    GET /assets/asset-20261004215731-mksvz7f.env (HTTP 401 without auth; with the same Bearer token):

    AWS_SECRET_ACCESS_KEY=POC-FAKE-SECRET-1234
    

The full chain: an agent-reachable MCP call copied the host user's .env into the workspace and the server served its contents back. validateAssetUploadPaths was active (the fixed build being exercised — the upload succeeded rather than being refused, which is the defect).

Impact

An adversary who can drive an agent that talks to SiYuan's MCP server (the exactly-the-threat-model of the parent advisory GHSA-p23f-cm6q-2qp8, which is why this is an incomplete fix and not a new class) reads any home-directory file whose name is not on the 17-entry dotfile list: first-order targets are .env, .env.local, .env.production, and the broader list omissions (~/Library/** on macOS, other users' home dirs, Windows AppData). Exfiltration requires no other primitive because the leaked file is served back inside the workspace the attacker can read.

Suggested fix

  1. Add .env / .env.* (and ~/Library/**, per-user home dirs beyond the current one, Windows AppData) to isSensitivePath, or better: invert the model so the MCP asset tool only accepts paths the operator explicitly granted (a path root), mirroring kernel/mcp/tools/file.go's workspace-relative restriction.
  2. Note the asymmetric copy-in design: a fix that returns "uploaded" while internally copying across a trust boundary means the correct minimal fix is to reject at fetchBytes/read time, not to rely on a name blacklist that must enumerate every secret-bearing filename in existence.

Severity

High

CVE ID

No known CVE

Weaknesses

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. Learn more on MITRE.

Credits