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:
.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.
- 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:
-
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).
-
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
-
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
- 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.
- 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.
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
validateAssetUploadPathsinkernel/mcp/tools/asset.go, which passes everyfilesargument of the MCP asset tool throughfilepath.Absand thenutil.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) walkssysPaths(system directories), Windows system prefixes, workspace-internalconf/temp, a home dotfile whitelist of 17 entries (.ssh,.aws,.kube,.docker,.gnupg,.git-credentials, ...) and names starting withcredentials/id_.Two blacklist gaps make the fix incomplete:
.envis 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.localetc. — any filename not on the 17-entry list). A deployment whose backend readsdotenv-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.~/Library/**(macOS Keychain), other users' home directories (/home/<otheruser>/) and Windows%APPDATA%/%LOCALAPPDATA%profile data — the same one-line-per-item omission class;.envis the finding we executed.After
validateAssetUploadPathsaccepts the path,asset uploadcontinues intoInsertLocalAssets(kernel/model/upload.go:178), which copies each listed local file into the workspacedata/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: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).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:
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):The full chain: an agent-reachable MCP call copied the host user's
.envinto the workspace and the server served its contents back.validateAssetUploadPathswas 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
.env/.env.*(and~/Library/**, per-user home dirs beyond the current one, Windows AppData) toisSensitivePath, or better: invert the model so the MCP asset tool only accepts paths the operator explicitly granted (a path root), mirroringkernel/mcp/tools/file.go's workspace-relative restriction.fetchBytes/read time, not to rely on a name blacklist that must enumerate every secret-bearing filename in existence.