Skip to content

Commit 7fa20b2

Browse files
feat: add HTTP Basic Auth for git operations and inject host git config (#174)
* feat: add HTTP Basic Auth for git operations and inject host git config This commit adds two key features to enable git operations in Docker containers: 1. **HTTP Basic Auth for Git Operations** - Added AuthEncoding enum with Simple and BasicAuthWithToken variants - Updated proxy rules to use BasicAuthWithToken for github.com - Encodes credentials as "Basic base64(x-access-token:TOKEN)" - Enables git push/pull/clone through the HTTPS proxy - Added comprehensive unit tests for Basic Auth encoding 2. **Git User Configuration Injection** - Added read_git_user_config() to read host git config - Injects GIT_AUTHOR_NAME, GIT_COMMITTER_NAME environment variables - Injects GIT_AUTHOR_EMAIL, GIT_COMMITTER_EMAIL environment variables - Ensures git commits in containers have proper attribution - Added tests for git config injection and graceful handling Files modified: - src/proxy/rules.rs: HTTP Basic Auth implementation - src/backends/docker.rs: Git config reading and injection - Cargo.toml: Added base64 dependency - tests/*: Updated all tests with new parameters 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * security: sanitize git config values to prevent injection attacks Added sanitize_git_config_value() function that removes control characters (except tabs) to prevent environment variable injection. - Filters out newlines that could inject malicious env vars - Removes other control characters that could cause issues - Preserves tabs as they're valid in git names - Added 4 tests to verify sanitization works correctly Addresses security concern from automated code review. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 8da1f8f commit 7fa20b2

6 files changed

Lines changed: 345 additions & 1 deletion

File tree

packages/multiplexer/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ fs2 = "0.4"
5959
# HTTP Proxy with TLS interception
6060
hudsucker = { version = "0.24", default-features = false, features = ["rcgen-ca", "rustls-client"] }
6161
http = "1"
62+
base64 = "0.22"
6263

6364
# TLS
6465
rustls = "0.23"

packages/multiplexer/src/backends/docker.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,56 @@ use tokio::process::Command;
44

55
use super::traits::ExecutionBackend;
66

7+
/// Sanitize git config value to prevent environment variable injection
8+
///
9+
/// Removes newlines and other control characters that could be used for injection attacks
10+
fn sanitize_git_config_value(value: &str) -> String {
11+
value
12+
.chars()
13+
.filter(|c| !c.is_control() || *c == '\t')
14+
.collect()
15+
}
16+
17+
/// Read git user configuration from the host system
18+
///
19+
/// Returns (user.name, user.email) if available from git config
20+
/// Values are sanitized to prevent environment variable injection
21+
async fn read_git_user_config() -> (Option<String>, Option<String>) {
22+
let name = Command::new("git")
23+
.args(["config", "--get", "user.name"])
24+
.output()
25+
.await
26+
.ok()
27+
.and_then(|output| {
28+
if output.status.success() {
29+
String::from_utf8(output.stdout)
30+
.ok()
31+
.map(|s| sanitize_git_config_value(s.trim()))
32+
.filter(|s| !s.is_empty())
33+
} else {
34+
None
35+
}
36+
});
37+
38+
let email = Command::new("git")
39+
.args(["config", "--get", "user.email"])
40+
.output()
41+
.await
42+
.ok()
43+
.and_then(|output| {
44+
if output.status.success() {
45+
String::from_utf8(output.stdout)
46+
.ok()
47+
.map(|s| sanitize_git_config_value(s.trim()))
48+
.filter(|s| !s.is_empty())
49+
} else {
50+
None
51+
}
52+
});
53+
54+
(name, email)
55+
}
56+
757
/// Detect if a directory is a git worktree and return the parent .git directory path
858
///
959
/// # Errors
@@ -172,6 +222,8 @@ impl DockerBackend {
172222
proxy_config: Option<&DockerProxyConfig>,
173223
print_mode: bool,
174224
images: &[String],
225+
git_user_name: Option<&str>,
226+
git_user_email: Option<&str>,
175227
) -> anyhow::Result<Vec<String>> {
176228
let container_name = format!("mux-{name}");
177229
let escaped_prompt = initial_prompt.replace('\'', "'\\''");
@@ -338,6 +390,25 @@ impl DockerBackend {
338390
}
339391
}
340392

393+
// Git user configuration from host
394+
// Set both AUTHOR and COMMITTER variables so git commits have proper attribution
395+
if let Some(name) = git_user_name {
396+
args.extend([
397+
"-e".to_string(),
398+
format!("GIT_AUTHOR_NAME={}", name),
399+
"-e".to_string(),
400+
format!("GIT_COMMITTER_NAME={}", name),
401+
]);
402+
}
403+
if let Some(email) = git_user_email {
404+
args.extend([
405+
"-e".to_string(),
406+
format!("GIT_AUTHOR_EMAIL={}", email),
407+
"-e".to_string(),
408+
format!("GIT_COMMITTER_EMAIL={}", email),
409+
]);
410+
}
411+
341412
// NOTE: We intentionally do NOT create a fake .credentials.json file.
342413
// The ANTHROPIC_API_KEY env var is sufficient and avoids validation issues.
343414
// When a credentials file exists, Claude Code validates it against the API,
@@ -477,6 +548,9 @@ impl ExecutionBackend for DockerBackend {
477548
None
478549
};
479550

551+
// Read git user configuration from the host
552+
let (git_user_name, git_user_email) = read_git_user_config().await;
553+
480554
let args = Self::build_create_args(
481555
name,
482556
workdir,
@@ -485,6 +559,8 @@ impl ExecutionBackend for DockerBackend {
485559
proxy_config_ref,
486560
options.print_mode,
487561
&options.images,
562+
git_user_name.as_deref(),
563+
git_user_email.as_deref(),
488564
)?;
489565
let output = Command::new("docker")
490566
.args(&args)
@@ -637,6 +713,8 @@ mod tests {
637713
None,
638714
false, // interactive mode
639715
&[], // no images
716+
None, // git user name
717+
None, // git user email
640718
).expect("Failed to build args");
641719

642720
// Must have -dit for interactive TTY sessions
@@ -651,6 +729,43 @@ mod tests {
651729
);
652730
}
653731

732+
/// Test sanitization of git config values to prevent injection attacks
733+
#[test]
734+
fn test_sanitize_git_config_removes_newlines() {
735+
// Test newline injection attempt
736+
let malicious = "John Doe\nGIT_EVIL=injected";
737+
let sanitized = sanitize_git_config_value(malicious);
738+
assert_eq!(sanitized, "John DoeGIT_EVIL=injected");
739+
assert!(!sanitized.contains('\n'));
740+
}
741+
742+
#[test]
743+
fn test_sanitize_git_config_removes_control_chars() {
744+
// Test various control characters
745+
let malicious = "user\x00name\x01with\x02control";
746+
let sanitized = sanitize_git_config_value(malicious);
747+
assert!(!sanitized.contains('\x00'));
748+
assert!(!sanitized.contains('\x01'));
749+
assert!(!sanitized.contains('\x02'));
750+
assert_eq!(sanitized, "usernamewithcontrol");
751+
}
752+
753+
#[test]
754+
fn test_sanitize_git_config_preserves_tabs() {
755+
// Tabs should be preserved as they're valid in names
756+
let with_tab = "John\tDoe";
757+
let sanitized = sanitize_git_config_value(with_tab);
758+
assert_eq!(sanitized, "John\tDoe");
759+
}
760+
761+
#[test]
762+
fn test_sanitize_git_config_preserves_normal_chars() {
763+
// Normal characters should pass through
764+
let normal = "John Doe <john@example.com>";
765+
let sanitized = sanitize_git_config_value(normal);
766+
assert_eq!(sanitized, normal);
767+
}
768+
654769
/// Test that docker run includes --user flag with non-root UID
655770
#[test]
656771
fn test_create_runs_as_non_root() {

0 commit comments

Comments
 (0)