Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/harness-tools/src/memory_include.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
50 changes: 50 additions & 0 deletions crates/harness-tools/src/memory_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down
Loading