{"record":{"id":"00fabcb95e11ebf4","repo":"Hmbown/CodeWhale","slug":"workspace-path-cannot-contain-components","errorCode":null,"errorMessage":"workspace path cannot contain '..' components","messagePattern":"workspace path cannot contain '\\.\\.' components","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp.rs","lineNumber":3839,"sourceCode":"fn workspace_allows_project_mcp_config(workspace: &Path) -> bool {\n    crate::config::is_workspace_trusted(workspace)\n}\n\nfn checked_workspace_mcp_config_path(workspace: &Path) -> Result<PathBuf> {\n    Ok(checked_workspace_path(workspace)?\n        .join(\".codewhale\")\n        .join(\"mcp.json\"))\n}\n\nfn checked_workspace_path(workspace: &Path) -> Result<PathBuf> {\n    if workspace.as_os_str().is_empty() {\n        anyhow::bail!(\"workspace path cannot be empty\");\n    }\n    if workspace\n        .components()\n        .any(|component| matches!(component, Component::ParentDir))\n    {\n        anyhow::bail!(\"workspace path cannot contain '..' components\");\n    }\n    let absolute = if workspace.is_absolute() {\n        workspace.to_path_buf()\n    } else {\n        std::env::current_dir()\n            .context(\"failed to resolve current directory for workspace\")?\n            .join(workspace)\n    };\n    match absolute.canonicalize() {\n        Ok(path) => Ok(path),\n        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {\n            Ok(normalize_path_components(&absolute))\n        }\n        Err(err) => {\n            Err(err).with_context(|| format!(\"failed to resolve workspace {}\", workspace.display()))\n        }\n    }\n}","sourceCodeStart":3821,"sourceCodeEnd":3857,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp.rs#L3821-L3857","documentation":"checked_workspace_path rejects workspace paths containing any ParentDir ('..') component. Because the workspace root itself would be ambiguous after traversal, any '..' anywhere in the input is refused before canonicalization, regardless of whether normalization would resolve it back inside.","triggerScenarios":"Passing a workspace like /home/me/proj/../proj2 or a relative ../repo into project MCP config resolution.","commonSituations":"Shell scripts composing paths with '..'; user-supplied --workspace arguments; config files storing unnormalized paths.","solutions":["Canonicalize the workspace path (std::fs::canonicalize) before passing it in","Normalize away '..' components at the boundary where user input is accepted","Reject '..' early in CLI validation with a clearer message"],"exampleFix":"// before\nlet ws = std::path::PathBuf::from(\"/home/me/proj/../proj2\");\nlet cfg = checked_workspace_mcp_config_path(&ws)?;\n\n// after\nlet ws = std::fs::canonicalize(\"/home/me/proj/../proj2\")?;\nlet cfg = checked_workspace_mcp_config_path(&ws)?;","handlingStrategy":"validation","validationCode":"// Canonicalize user-supplied workspaces before use:\nlet workspace = std::fs::canonicalize(&raw_workspace)\n    .unwrap_or_else(|_| normalize_path_components(&raw_workspace));\nanyhow::ensure!(!workspace.components().any(|c| matches!(c, std::path::Component::ParentDir)), \"workspace contains '..'\");","typeGuard":"fn is_usable_workspace(p: &std::path::Path) -> bool {\n    !p.as_os_str().is_empty()\n        && !p.components().any(|c| matches!(c, std::path::Component::ParentDir))\n}","tryCatchPattern":null,"preventionTips":["Canonicalize workspace paths at the input boundary (CLI, config load)","Compose workspace paths with joins on absolute bases instead of string concatenation with '..'","Add an integration test that feeds '..'-containing workspaces and asserts rejection"],"tags":["mcp","workspace","validation","path-traversal"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}