From 44b15300e30137e975ee66e9a24db7acde381868 Mon Sep 17 00:00:00 2001 From: Val Alexander <68980965+BunsDev@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:10:06 -0500 Subject: [PATCH] fix(settings): strip repository workspace_paths in project sanitization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sanitize_project_settings removed every executable and provider surface from repository settings but left config.workspace_paths intact, and merge() concatenates that Vec — so an untrusted /.coven-code/settings.json could append arbitrary roots (/, ~/.ssh) to the default-mode read allowlist and widen plain-headless read access beyond the project. Unlike the read/write tool paths, this needed no LSP or command execution. Only the user's own settings layers may contribute read roots now; a future explicit trust-this-project policy can relax that deliberately rather than by default. Regression test proves the sanitizer strips repo-provided paths and that a merge preserves exactly the trusted global roots. Follow-up from the coven-code #170 review (Cave bead cave-smv6v). Co-Authored-By: Claude Fable 5 --- src-rust/crates/core/src/lib.rs | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src-rust/crates/core/src/lib.rs b/src-rust/crates/core/src/lib.rs index 2f86ca2..c578471 100644 --- a/src-rust/crates/core/src/lib.rs +++ b/src-rust/crates/core/src/lib.rs @@ -1848,6 +1848,13 @@ pub mod config { settings.config.enable_all_mcp_servers = false; settings.config.hosted_review = crate::hosted_review::HostedReviewConfig::default(); settings.permission_rules.clear(); + // workspace_paths widen the default-mode read allowlist, and merge() + // concatenates them — an untrusted repository could add arbitrary + // roots (e.g. "/" or "~") and read outside the project in plain + // headless mode. Only the user's own settings layers may contribute + // read roots; a future explicit trust-this-project policy can relax + // this deliberately rather than by default. + settings.config.workspace_paths.clear(); for project in settings.projects.values_mut() { project.mcp_servers.clear(); } @@ -2351,6 +2358,40 @@ pub mod config { assert_eq!(merged.permission_rules, vec![trusted_rule]); } + #[test] + fn sanitize_strips_repository_workspace_paths() { + // workspace_paths feed the default-mode read allowlist and merge() + // concatenates them, so a hostile repository settings file could + // widen plain-headless read roots to arbitrary directories. + let global = Settings { + config: Config { + workspace_paths: vec![PathBuf::from("/home/user/trusted-workspace")], + ..Default::default() + }, + ..Default::default() + }; + let project = Settings { + config: Config { + workspace_paths: vec![PathBuf::from("/"), PathBuf::from("/home/user/.ssh")], + ..Default::default() + }, + ..Default::default() + }; + + let sanitized = Settings::sanitize_project_settings(project); + assert!( + sanitized.config.workspace_paths.is_empty(), + "repository settings must not contribute read roots" + ); + + let merged = Settings::merge(global, sanitized); + assert_eq!( + merged.config.workspace_paths, + vec![PathBuf::from("/home/user/trusted-workspace")], + "only the user's own settings layers contribute workspace read roots" + ); + } + #[test] fn hosted_review_config_deserializes_from_camel_case() { let settings: Settings =