Skip to content

Commit a973f60

Browse files
Refactor: define git commands as slices, not strings
Slices will be passed directly to the system call; strings will be interpreted by shell, where word splitting will occur. No word splitting and no shell is better for security, and more convenient because one level of quote escaping goes away. This is needed for SSH support because of security implications, as well as because SSH commands will be more complicated commit-id:1c53a1f1
1 parent 51c5bb9 commit a973f60

File tree

3 files changed

+104
-41
lines changed

3 files changed

+104
-41
lines changed

josh-proxy/src/lib.rs

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ pub mod juniper_hyper;
55
#[macro_use]
66
extern crate lazy_static;
77

8-
use josh::JoshError;
8+
use josh::{josh_error, JoshError};
9+
use std::fs;
910
use std::path::PathBuf;
1011

1112
#[derive(PartialEq)]
@@ -429,13 +430,16 @@ pub fn push_head_url(
429430
let shell = josh::shell::Shell {
430431
cwd: repo.path().to_owned(),
431432
};
433+
432434
let (username, password) = auth.parse()?;
433-
let cmd = format!(
434-
"git push {} {} '{}'",
435-
if force { "-f" } else { "" },
436-
&url,
437-
&spec
438-
);
435+
436+
let mut cmd = vec!["git", "push"];
437+
if force {
438+
cmd.push("--force")
439+
}
440+
cmd.push(&url);
441+
cmd.push(&spec);
442+
439443
let mut fakehead = repo.reference(&rn, oid, true, "push_head_url")?;
440444
let (stdout, stderr, status) = shell.command_env(
441445
&cmd,
@@ -460,7 +464,8 @@ fn create_repo_base(path: &PathBuf) -> josh::JoshResult<josh::shell::Shell> {
460464
git2::Repository::init_bare(&path)?;
461465

462466
let credential_helper =
463-
"'!f() { echo \"username=\"$GIT_USER\"\npassword=\"$GIT_PASSWORD\"\"; }; f'";
467+
r#"!f() { echo username="${GIT_USER}"; echo password="${GIT_PASSWORD}"; }; f"#;
468+
464469
let config_options = [
465470
("http.receivepack", "true"),
466471
("user.name", "josh"),
@@ -478,11 +483,29 @@ fn create_repo_base(path: &PathBuf) -> josh::JoshResult<josh::shell::Shell> {
478483
};
479484

480485
config_options
481-
.map(|(key, value)| shell.command(format!("git config {} {}", key, value).as_str()));
482-
483-
shell.command("rm -Rf hooks");
484-
shell.command("rm -Rf *.lock");
485-
shell.command("rm -Rf packed-refs");
486+
.iter()
487+
.map(
488+
|(key, value)| match shell.command(&["git", "config", key, value]) {
489+
(_, _, code) if code != 0 => Err(josh_error("failed to set git config value")),
490+
_ => Ok(()),
491+
},
492+
)
493+
.collect::<Result<Vec<_>, _>>()?;
494+
495+
[path.join("hooks"), path.join("packed-refs")]
496+
.iter()
497+
.filter(|p| p.exists())
498+
.map(|p| fs::remove_dir_all(p))
499+
.collect::<Result<Vec<_>, _>>()?;
500+
501+
// Delete all files ending with ".lock"
502+
fs::read_dir(path)?
503+
.filter_map(|entry| match entry {
504+
Ok(entry) if entry.path().ends_with(".lock") => Some(path),
505+
_ => None,
506+
})
507+
.map(|file| fs::remove_file(file))
508+
.collect::<Result<Vec<_>, _>>()?;
486509

487510
Ok(shell)
488511
}
@@ -495,7 +518,7 @@ pub fn create_repo(path: &std::path::Path) -> josh::JoshResult<()> {
495518
let overlay_path = path.join("overlay");
496519
tracing::debug!("init overlay repo: {:?}", overlay_path);
497520
let overlay_shell = create_repo_base(&overlay_path)?;
498-
overlay_shell.command("mkdir hooks");
521+
overlay_shell.command(&["mkdir", "hooks"]);
499522

500523
let josh_executable = std::env::current_exe().expect("can't find path to exe");
501524
std::os::unix::fs::symlink(
@@ -525,13 +548,14 @@ pub fn get_head(
525548
let shell = josh::shell::Shell {
526549
cwd: path.to_owned(),
527550
};
551+
528552
let (username, password) = auth.parse()?;
529553

530-
let cmd = format!("git ls-remote --symref {} {}", &url, "HEAD");
554+
let cmd = &["git", "ls-remote", "--symref", &url, "HEAD"];
531555
tracing::info!("get_head {:?} {:?} {:?}", cmd, path, "");
532556

533557
let (stdout, _stderr, _) = shell.command_env(
534-
&cmd,
558+
cmd,
535559
&[],
536560
&[("GIT_PASSWORD", &password), ("GIT_USER", &username)],
537561
);
@@ -579,7 +603,7 @@ pub fn fetch_refs_from_url(
579603
.iter()
580604
.map(|r| {
581605
format!(
582-
"'+{}:refs/josh/upstream/{}/{}'",
606+
"+{}:refs/josh/upstream/{}/{}",
583607
&r,
584608
josh::to_ns(upstream_repo),
585609
&r
@@ -591,7 +615,12 @@ pub fn fetch_refs_from_url(
591615
cwd: path.to_owned(),
592616
};
593617

594-
let cmd = format!("git fetch --prune --no-tags {} {}", &url, &specs.join(" "));
618+
let cmd = ["git", "fetch", "--prune", "--no-tags", &url]
619+
.map(str::to_owned)
620+
.to_vec();
621+
let cmd = cmd.into_iter().chain(specs.into_iter()).collect::<Vec<_>>();
622+
let cmd = cmd.iter().map(|s| s as &str).collect::<Vec<&str>>();
623+
595624
tracing::info!("fetch_refs_from_url {:?} {:?} {:?}", cmd, path, "");
596625

597626
let (username, password) = auth.parse().map_err(FetchError::from_josh_error)?;

src/housekeeping.rs

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,20 @@ pub fn memorize_from_to(
111111
Ok(((from, oid), to_ref))
112112
}
113113

114-
fn run_command(path: &Path, cmd: &str) -> String {
114+
fn run_command(path: &Path, cmd: &[&str]) -> String {
115115
let shell = shell::Shell {
116116
cwd: path.to_owned(),
117117
};
118118

119119
let output = "";
120120

121-
let (stdout, stderr, _) = shell.command(cmd);
121+
let (stdout, stderr, _) = shell.command(&cmd);
122122
let output = format!(
123123
"{}\n\n{}:\nstdout:\n{}\n\nstderr:{}\n",
124-
output, cmd, stdout, stderr
124+
output,
125+
cmd.join(" "),
126+
stdout,
127+
stderr
125128
);
126129

127130
output
@@ -322,11 +325,19 @@ pub fn run(repo_path: &std::path::Path, do_gc: bool) -> JoshResult<()> {
322325

323326
info!(
324327
"{}",
325-
run_command(transaction_mirror.repo().path(), "git count-objects -v").replace("\n", " ")
328+
run_command(
329+
transaction_mirror.repo().path(),
330+
&["git", "count-objects", "-v"]
331+
)
332+
.replace("\n", " ")
326333
);
327334
info!(
328335
"{}",
329-
run_command(transaction_overlay.repo().path(), "git count-objects -v").replace("\n", " ")
336+
run_command(
337+
transaction_overlay.repo().path(),
338+
&["git", "count-objects", "-v"]
339+
)
340+
.replace("\n", " ")
330341
);
331342
if !std::env::var("JOSH_NO_DISCOVER").is_ok() {
332343
housekeeping::discover_filter_candidates(&transaction_mirror)?;
@@ -339,33 +350,50 @@ pub fn run(repo_path: &std::path::Path, do_gc: bool) -> JoshResult<()> {
339350
"\n----------\n{}\n----------",
340351
run_command(
341352
transaction_mirror.repo().path(),
342-
"git repack -dkbn --no-write-bitmap-index --threads=4"
353+
&[
354+
"git",
355+
"repack",
356+
"-dn",
357+
"--keep-unreachable",
358+
"--no-write-bitmap-index",
359+
"--threads=4"
360+
]
343361
)
344362
);
345363
info!(
346364
"\n----------\n{}\n----------",
347365
run_command(
348366
transaction_mirror.repo().path(),
349-
"git multi-pack-index write --bitmap"
367+
&["git", "multi-pack-index", "write", "--bitmap"]
350368
)
351369
);
352370
info!(
353371
"\n----------\n{}\n----------",
354372
run_command(
355373
transaction_overlay.repo().path(),
356-
"git repack -dkbn --no-write-bitmap-index --threads=4"
374+
&[
375+
"git",
376+
"repack",
377+
"-dn",
378+
"--keep-unreachable",
379+
"--no-write-bitmap-index",
380+
"--threads=4"
381+
]
357382
)
358383
);
359384
info!(
360385
"\n----------\n{}\n----------",
361386
run_command(
362387
transaction_overlay.repo().path(),
363-
"git multi-pack-index write --bitmap"
388+
&["git", "multi-pack-index", "write", "--bitmap"]
364389
)
365390
);
366391
info!(
367392
"\n----------\n{}\n----------",
368-
run_command(transaction_mirror.repo().path(), "git count-objects -vH")
393+
run_command(
394+
transaction_mirror.repo().path(),
395+
&["git", "count-objects", "-vH"]
396+
)
369397
);
370398
}
371399
Ok(())

src/shell.rs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ pub struct Shell {
1010
}
1111

1212
impl Shell {
13-
pub fn command(&self, cmd: &str) -> (String, String, i32) {
13+
pub fn command(&self, cmd: &[&str]) -> (String, String, i32) {
1414
self.command_env(cmd, &[], &[])
1515
}
1616

1717
#[tracing::instrument(skip(self, env_notrace))]
1818
pub fn command_env(
1919
&self,
20-
cmd: &str,
20+
cmd: &[&str],
2121
env: &[(&str, &str)],
2222
env_notrace: &[(&str, &str)],
2323
) -> (String, String, i32) {
@@ -27,21 +27,25 @@ impl Shell {
2727
self.cwd.to_path_buf()
2828
};
2929

30-
let mut command = Command::new("sh");
30+
let env = env.to_owned();
31+
let env_notrace = env_notrace.to_owned();
32+
33+
let (cmd, args) = {
34+
if let [cmd, args @ ..] = cmd {
35+
(cmd, args)
36+
} else {
37+
panic!("No command provided")
38+
}
39+
};
40+
41+
let mut command = Command::new(cmd);
3142
command
3243
.current_dir(&self.cwd)
33-
.arg("-c")
34-
.arg(&cmd)
44+
.args(args)
45+
.envs(env)
46+
.envs(env_notrace)
3547
.env("GIT_DIR", &git_dir);
3648

37-
for (k, v) in env.iter() {
38-
command.env(&k, &v);
39-
}
40-
41-
for (k, v) in env_notrace.iter() {
42-
command.env(&k, &v);
43-
}
44-
4549
let output = command
4650
.output()
4751
.unwrap_or_else(|e| panic!("failed to execute process: {}\n{}", cmd, e));
@@ -50,10 +54,12 @@ impl Shell {
5054
.expect("failed to decode utf8")
5155
.trim()
5256
.to_string();
57+
5358
let stderr = String::from_utf8(output.stderr)
5459
.expect("failed to decode utf8")
5560
.trim()
5661
.to_string();
62+
5763
tracing::event!(Level::TRACE, ?stdout, ?stderr);
5864
(stdout, stderr, output.status.code().unwrap_or(1))
5965
}

0 commit comments

Comments
 (0)