From 596ae32eb561c466432f71727034863be44b2335 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 01:29:17 +0000 Subject: [PATCH] Harden memory.sync git transport against ext::/fd:: command execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin GIT_ALLOW_PROTOCOL on every git invocation in memory_sync's run_git, mirroring memory_include. memory.sync_setup already validates remote_url via validate_git_url, but memory.sync's push/pull act on an already-stored remote.origin.url that never re-passes validation — an out-of-band remote set to an ext::/fd:: transport could still execute arbitrary commands. Exporting GIT_ALLOW_PROTOCOL refuses those protocols at the git layer as defence in depth. Adds a regression test that configures an ext:: remote directly (bypassing the validator) and asserts the smuggled command never runs. Fixes #51 https://claude.ai/code/session_01UxZiFGdjUnrqLitT3S92uC --- crates/harness-tools/src/memory_include.rs | 2 +- crates/harness-tools/src/memory_sync.rs | 50 ++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/crates/harness-tools/src/memory_include.rs b/crates/harness-tools/src/memory_include.rs index 6e45888..a7b4d81 100644 --- a/crates/harness-tools/src/memory_include.rs +++ b/crates/harness-tools/src/memory_include.rs @@ -45,7 +45,7 @@ const GIT_TIMEOUT_MS: u64 = 60_000; /// `ext::` / `fd::` transports (which execute arbitrary commands and /// are honoured by default for a directly-invoked clone) can never /// fire from attacker-controlled memory content. -const GIT_ALLOW_PROTOCOL: &str = "https:ssh:git:file"; +pub(crate) const GIT_ALLOW_PROTOCOL: &str = "https:ssh:git:file"; /// Reject git URLs that could lead to command execution or option /// smuggling before they ever reach `git clone`. diff --git a/crates/harness-tools/src/memory_sync.rs b/crates/harness-tools/src/memory_sync.rs index 2f4ec48..8ec58b1 100644 --- a/crates/harness-tools/src/memory_sync.rs +++ b/crates/harness-tools/src/memory_sync.rs @@ -126,6 +126,13 @@ async fn run_git( cmd.arg("-C") .arg(cwd) .args(args) + // Defence in depth against git's command-executing transports + // (`ext::` / `fd::`). `sync_setup` validates the `remote_url` + // up front, but `sync`'s push/pull operate on an already-stored + // `remote.origin.url` that never re-passes validation — so pin + // the allowed protocol set on every git invocation. Mirrors + // `memory_include::run_git`. + .env("GIT_ALLOW_PROTOCOL", crate::memory_include::GIT_ALLOW_PROTOCOL) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -1142,6 +1149,49 @@ mod tests { assert!(err.to_string().contains("remote-helper transport")); } + #[tokio::test] + async fn run_git_refuses_ext_transport_configured_out_of_band() { + // Defence in depth: `sync_setup` validates `remote_url`, but + // `sync`'s push/pull act on an already-stored + // `remote.origin.url`. Configure an `ext::` transport directly + // (bypassing the validator, as an out-of-band edit would) and + // assert `run_git` refuses to execute the smuggled command — + // proving `GIT_ALLOW_PROTOCOL` is pinned on every invocation. + if StdCommand::new("git").arg("--version").output().is_err() { + return; + } + let dir = tempdir().unwrap(); + git_init(dir.path()); + fs::write(dir.path().join("MEMORY.md"), "- seed\n") + .await + .unwrap(); + commit_all(dir.path(), "seed").await; + + // A marker file the smuggled command would create if git + // honoured the `ext::` transport. + let pwned = dir.path().join("PWNED"); + let payload = format!("ext::sh -c touch{}{}", " ", pwned.display()); + StdCommand::new("git") + .arg("-C") + .arg(dir.path()) + .args(["remote", "add", "origin"]) + .arg(&payload) + .status() + .unwrap(); + + // Pushing to the malicious remote must fail on the blocked + // protocol rather than run the command. + let (ok, _out, err) = + run_git(dir.path(), &["push", "origin", "main"], 10_000) + .await + .unwrap(); + assert!(!ok, "push to an ext:: remote must not succeed"); + assert!( + !pwned.exists(), + "ext:: transport executed the smuggled command (GIT_ALLOW_PROTOCOL not enforced); stderr: {err}" + ); + } + #[tokio::test] async fn auto_sync_ticker_runs_initial_pull_then_periodic() { // Closes the loop end-to-end: a local bare repo, one client