{"record":{"id":"89ac00a74b9fb5e4","repo":"can1357/oh-my-pi","slug":"refusing-to-download-outside-the-workspace-down","errorCode":null,"errorMessage":"Refusing to download outside the workspace: ${downloadPath}","messagePattern":"Refusing to download outside the workspace: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/cursor.ts","lineNumber":825,"sourceCode":"\t\tconst texts = textItems.map(item => item.text as string);\n\t\tconst blobItem = read.contents.find(item => item.blob !== undefined);\n\t\tconst blob = blobItem?.blob;\n\t\tconst textMimeType = textItems[0]?.mimeType;\n\t\tconst blobMimeType = blobItem?.mimeType;\n\n\t\tif (downloadPath) {\n\t\t\t// Text resources download as their own bytes; a blob decodes first.\n\t\t\tconst payload =\n\t\t\t\ttexts.length > 0 ? texts.join(\"\\n\") : blob !== undefined ? Buffer.from(blob, \"base64\") : undefined;\n\t\t\tif (payload === undefined) return null;\n\t\t\t// The path is workspace-relative BY CONTRACT, but it arrives from the\n\t\t\t// server, and `resolveToCwd` deliberately honors absolute paths and\n\t\t\t// `..` for user-authored tool input. Taking it at its word would let a\n\t\t\t// frame write anywhere this process can reach, so confine it here\n\t\t\t// rather than trusting the declaration.\n\t\t\tconst cwd = this.options.getCwd?.() ?? this.options.cwd;\n\t\t\tconst absolutePath = confineToWorkspace(downloadPath, cwd);\n\t\t\tif (!absolutePath) throw new Error(`Refusing to download outside the workspace: ${downloadPath}`);\n\t\t\tawait writeWithoutFollowingLinks(absolutePath, payload);\n\t\t\t// The path echoed back is the one the frame asked for; the model\n\t\t\t// addresses it the same relative way.\n\t\t\treturn { uri, mimeType: texts.length > 0 ? textMimeType : blobMimeType, downloadPath };\n\t\t}\n\n\t\tif (texts.length > 0) return { uri, mimeType: textMimeType, text: texts.join(\"\\n\") };\n\t\tif (blob === undefined) return null;\n\t\treturn { uri, mimeType: blobMimeType, blob: Buffer.from(blob, \"base64\") };\n\t}\n\n\t/**\n\t * Settle a completed native Cursor todo call, mirroring its list when the\n\t * server supplied an authoritative one.\n\t *\n\t * Cursor's snapshot is a flat list, so tasks already known locally keep\n\t * their phase and only their status is updated; unknown tasks land in a\n\t * single fallback phase. Statuses come straight from the server snapshot —","sourceCodeStart":807,"sourceCodeEnd":843,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/cursor.ts#L807-L843","documentation":"Resource download paths declared by MCP frames are user-authoritative, and resolveToCwd deliberately honors absolute paths and '..'. To prevent a remote server frame from writing anywhere the process can reach, the handler confines the path to the workspace with confineToWorkspace; if the path escapes it, the download is refused. This is a security boundary against path traversal.","triggerScenarios":"A downloadPath that resolves outside the session working directory — an absolute path elsewhere on disk, or a relative path containing enough '..' segments to escape the workspace (classic path traversal, possibly from a malicious or misconfigured MCP server frame).","commonSituations":"An MCP server returns a resource with a suggested absolute save path (/tmp/... or C:\\...); a crafted resource URI includes ../ traversal; the user runs the agent from a narrower cwd than the file's real location.","solutions":["Use a path that resolves inside the workspace (relative path without leading '..' escape).","If the file legitimately belongs outside the workspace, start the session with cwd at (or above) the intended target directory.","Strip or sanitize the server-provided path in your MCP client wrapper; only pass user-approved relative names.","If traversal came from a third-party MCP server, treat it as untrusted behavior and restrict or drop that server."],"exampleFix":"// before: escapes the workspace\n{ downloadPath: \"../../etc/hosts.dl\" }\n// after: confined to workspace\n{ downloadPath: \"downloads/hosts.dl\" }","handlingStrategy":"validation","validationCode":"import path from \"node:path\";\nfunction staysInWorkspace(p: string, cwd: string): boolean {\n  const abs = path.resolve(cwd, p);\n  return abs === cwd || abs.startsWith(cwd + path.sep);\n}\nif (!staysInWorkspace(downloadPath, cwd)) {\n  // rewrite or reject the path before calling readMcpResource\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await readMcpResource({ server, uri, downloadPath });\n} catch (e) {\n  if (String(e.message).startsWith(\"Refusing to download outside the workspace\")) {\n    // fall back to a workspace-relative filename derived from the resource URI\n    return readMcpResource({ server, uri, downloadPath: safeWorkspaceName(uri) });\n  } else throw e;\n}","preventionTips":["Treat server-supplied download paths as untrusted; never pass absolute paths from frames through unmodified.","Sanitize '..' segments and absolute prefixes before issuing a download.","Run sessions with cwd set to the tree you want downloads confined to.","Audit third-party MCP servers for resource paths that attempt traversal."],"tags":["security","path-traversal","mcp"],"backgroundTag":"path-traversal-blocked","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}