{"record":{"id":"f0f360cbec3994c1","repo":"Hmbown/CodeWhale","slug":"reviewed-plugin-mcp-argument-path-escaped-its-stag","errorCode":null,"errorMessage":"reviewed plugin MCP argument path escaped its staged root","messagePattern":"reviewed plugin MCP argument path escaped its staged root","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp.rs","lineNumber":3798,"sourceCode":"fn freeze_plugin_stdio_paths(config: &mut McpServerConfig, staged_root: &Path) -> Result<()> {\n    if let Some(command) = config.command.as_mut()\n        && (command.contains('/') || command.contains('\\\\'))\n    {\n        let frozen = resolve_plugin_mcp_cwd(staged_root, Some(Path::new(command)))?;\n        *command = frozen.display().to_string();\n    }\n    let runtime_cwd = config.cwd.as_deref().unwrap_or(staged_root).to_path_buf();\n    for argument in &mut config.args {\n        if argument.starts_with('-') || Path::new(argument).is_absolute() {\n            continue;\n        }\n        let candidate = normalize_path_components(&runtime_cwd.join(argument.as_str()));\n        if candidate.exists() {\n            let frozen = candidate\n                .canonicalize()\n                .context(\"failed to freeze reviewed plugin MCP argument path\")?;\n            if !frozen.starts_with(staged_root) {\n                anyhow::bail!(\"reviewed plugin MCP argument path escaped its staged root\");\n            }\n            *argument = frozen.display().to_string();\n        }\n    }\n    Ok(())\n}\n\nfn resolve_plugin_mcp_cwd(plugin_path: &Path, cwd: Option<&Path>) -> Result<PathBuf> {\n    let cwd = match cwd {\n        Some(cwd) if cwd.is_relative() => normalize_path_components(&plugin_path.join(cwd)),\n        Some(cwd) => normalize_path_components(cwd),\n        None => plugin_path.to_path_buf(),\n    };\n    let resolved = cwd\n        .canonicalize()\n        .unwrap_or_else(|_| normalize_path_components(&cwd));\n    if !resolved.starts_with(plugin_path) {\n        anyhow::bail!(\"reviewed plugin MCP path escaped its staged root\");","sourceCodeStart":3780,"sourceCodeEnd":3816,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp.rs#L3780-L3816","documentation":"For reviewed (sandboxed) plugin MCP servers, any relative command argument that names an existing path is resolved against the runtime cwd, canonicalized, and then required to stay inside the plugin's staged root directory. If the canonical path (after resolving symlinks and '..') lands outside the staging directory, the server config is rejected - a reviewed plugin cannot smuggle execution outside its staged tree via argument paths.","triggerScenarios":"A plugin manifest passes a relative arg like '../../bin/helper', or an arg that is or crosses a symlink pointing outside the staged root, and that path exists on disk so the freeze step runs.","commonSituations":"A plugin that invokes bundled binaries by relative path but was staged with symlinks inside the tree; moving the staging directory so previously-fine relative paths now resolve elsewhere.","solutions":["Change the plugin's args to reference files inside the plugin's staged directory only","Remove symlinks inside the staged tree that point outside it, then re-stage and re-review the plugin","Invoke external tools by bare name (PATH lookup) instead of file paths - PATH args skip the freeze check"],"exampleFix":"// before (plugin manifest, server args)\n\"args\": [\"../../bin/helper\", \"--serve\"]\n\n// after\n\"args\": [\"./bin/helper\", \"--serve\"]","handlingStrategy":"validation","validationCode":"// Before enabling a reviewed plugin MCP server, verify every relative arg stays in the staged root:\nfor arg in &server_config.args {\n    if arg.starts_with('-') || std::path::Path::new(arg).is_absolute() { continue; }\n    let candidate = runtime_cwd.join(arg);\n    if candidate.exists() {\n        let frozen = candidate.canonicalize()?;\n        anyhow::ensure!(frozen.starts_with(&staged_root), \"arg escapes staged root: {arg}\");\n    }\n}","typeGuard":"fn plugin_args_within_root(args: &[String], cwd: &std::path::Path, root: &std::path::Path) -> bool {\n    args.iter().all(|a| {\n        a.starts_with('-')\n            || std::path::Path::new(a).is_absolute()\n            || !cwd.join(a).exists()\n            || cwd.join(a).canonicalize().map(|c| c.starts_with(root)).unwrap_or(false)\n    })\n}","tryCatchPattern":null,"preventionTips":["Author plugin manifests with args that only reference files inside the staged tree","Avoid symlinks inside staged plugin directories, especially absolute or out-of-tree targets","Re-stage and re-review the plugin after any manifest or tree change so paths are re-frozen"],"tags":["mcp","plugins","security","path-traversal"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}