Skip to content
Merged
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
101 changes: 101 additions & 0 deletions tests/security_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

//! Security tests to ensure robustness against various attack vectors.

use std::fs;

mod common;
use common::TestHarness;

Expand Down Expand Up @@ -98,4 +100,103 @@ mod tests {
// Verify it worked
assert!(harness.dir_exists("sub/-sparse/src"));
}

/// CVE-2018-17456-class option injection: a submodule name or URL that mimics
/// a `git clone`/`git submodule` flag (e.g. `--upload-pack=<cmd>`) must be
/// treated as inert data, never as an option that triggers command execution.
///
/// submod drives git via gix/git2 (no shell) and uses `--` before the URL/path
/// on the CLI last-resort path, so the payload should never run. This test is
/// non-vacuous: if any code path ever shelled the name/URL out unsafely, the
/// sentinel file would be created.
#[test]
fn test_flag_like_name_and_url_do_not_inject_commands() {
let harness = TestHarness::new().expect("Failed to create test harness");
harness.init_git_repo().expect("Failed to init git repo");

let remote_repo = harness
.create_test_remote("inject-remote")
.expect("Failed to create remote");
let remote_url = format!("file://{}", remote_repo.display());

// Sentinels: `touch <relative>` would land in cwd (the working tree);
// `touch <absolute>` would land at a fixed path. Neither must appear.
let rel_sentinel = harness.work_dir.join("INJECTED_BY_NAME");
let abs_sentinel = harness.work_dir.join("INJECTED_BY_URL");
assert!(!rel_sentinel.exists() && !abs_sentinel.exists());

// Flag-like NAME (passed via `--name=` so clap accepts the leading dashes).
let _ = harness
.run_submod(&[
"add",
&remote_url,
"--name=--upload-pack=touch INJECTED_BY_NAME",
"--path",
"lib/inj-name",
])
.expect("Failed to run submod");
assert!(
!rel_sentinel.exists(),
"a flag-like submodule name must not inject a command"
);

// Malicious transport URL (ext:: would run a shell command if honored).
let evil_url = format!("ext::sh -c \"touch {}\"", abs_sentinel.display());
let _ = harness
.run_submod(&[
"add",
&evil_url,
"--name",
"inj-url",
"--path",
"lib/inj-url",
])
.expect("Failed to run submod");
assert!(
!abs_sentinel.exists(),
"a malicious transport URL must not execute a command"
);
}

/// A hostile `.gitmodules` fed to `generate-config --from-setup` must be parsed
/// as data only: its url/branch values are serialized verbatim into the output
/// config, never executed. Non-vacuous: the generated file must actually contain
/// the hostile values (proving the parse path ran) while no sentinel is created.
#[test]
fn test_generate_config_from_malicious_gitmodules_does_not_execute() {
let harness = TestHarness::new().expect("Failed to create test harness");
harness.init_git_repo().expect("Failed to init git repo");

let sentinel = harness.work_dir.join("GC_INJECTED");
let gitmodules = format!(
"[submodule \"evil\"]\n\tpath = lib/evil\n\turl = ext::sh -c \"touch {}\"\n\tbranch = --upload-pack=touch {}\n",
sentinel.display(),
sentinel.display()
);
fs::write(harness.work_dir.join(".gitmodules"), gitmodules)
.expect("Failed to write .gitmodules");

harness
.run_submod(&[
"generate-config",
"--from-setup",
"--output",
"out.toml",
"--force",
])
.expect("Failed to run submod");

assert!(
!sentinel.exists(),
"generate-config must not execute values read from .gitmodules"
);

// Non-vacuity: the hostile entry was actually parsed and captured as data.
let generated = fs::read_to_string(harness.work_dir.join("out.toml"))
.expect("generate-config should have written the output config");
assert!(
generated.contains("ext::sh -c"),
"expected the hostile url to be captured as inert data, got:\n{generated}"
);
}
}
Loading