mod common;
use gunnar_ssh::Access;
use common::{authenticate, exec, exec_with_env, mint_key, start_with_key, EnvOutcome};
const UPLOAD_PACK: &str = "git-upload-pack 'nordisk/osm-katana.git'";
#[tokio::test]
async fn git_protocol_is_accepted_and_reaches_the_wire_seam() {
let key = mint_key();
let fixture = start_with_key(&key, Access::Read).await;
let session = authenticate(fixture.server.local_addr(), &key, "git")
.await
.expect("connect")
.expect("authenticate");
let (env, outcome) = exec_with_env(&session, &[("GIT_PROTOCOL", "version=2")], UPLOAD_PACK)
.await
.expect("exec");
assert_eq!(
env,
vec![EnvOutcome::Accepted],
"GIT_PROTOCOL must be accepted; it is how a client asks for v2"
);
assert!(
matches!(&outcome, common::ExecOutcome::Accepted { .. }),
"{outcome:?}"
);
assert_eq!(
fixture.transport.git_protocols(),
vec![Some("version=2".to_string())],
"the allowlisted value must reach the seam, or accepting it changes nothing"
);
fixture
.server
.shutdown("test over")
.await
.expect("shutdown");
}
#[tokio::test]
async fn a_variable_that_is_not_on_the_allowlist_is_refused() {
let key = mint_key();
let fixture = start_with_key(&key, Access::Read).await;
let session = authenticate(fixture.server.local_addr(), &key, "git")
.await
.expect("connect")
.expect("authenticate");
let hostile = [
("LD_PRELOAD", "/tmp/evil.so"),
("LD_LIBRARY_PATH", "/tmp"),
("PATH", "/tmp/bin"),
("GIT_DIR", "/etc"),
("GIT_ALTERNATE_OBJECT_DIRECTORIES", "/etc"),
("GIT_CONFIG", "/etc/passwd"),
("RUST_LOG", "trace"),
("GIT_PROTOCOL_", "version=2"),
("git_protocol", "version=2"),
("XGIT_PROTOCOL", "version=2"),
];
for (name, value) in hostile {
let (env, _) = exec_with_env(&session, &[(name, value)], UPLOAD_PACK)
.await
.expect("exec");
assert_eq!(
env,
vec![EnvOutcome::Refused],
"{name} was not refused; gunnar's environment is an allowlist of one"
);
}
assert!(
fixture
.transport
.git_protocols()
.iter()
.all(|value| value.is_none()),
"a refused variable reached the seam anyway: {:?}",
fixture.transport.git_protocols()
);
fixture
.server
.shutdown("test over")
.await
.expect("shutdown");
}
#[tokio::test]
async fn a_client_that_sends_no_environment_carries_no_protocol() {
let key = mint_key();
let fixture = start_with_key(&key, Access::Read).await;
let session = authenticate(fixture.server.local_addr(), &key, "git")
.await
.expect("connect")
.expect("authenticate");
let outcome = exec(&session, UPLOAD_PACK).await.expect("exec");
assert!(
matches!(&outcome, common::ExecOutcome::Accepted { .. }),
"{outcome:?}"
);
assert_eq!(
fixture.transport.git_protocols(),
vec![None],
"a session with no GIT_PROTOCOL must reach the seam with no protocol at all"
);
fixture
.server
.shutdown("test over")
.await
.expect("shutdown");
}
#[tokio::test]
async fn an_implausibly_long_value_is_refused() {
let key = mint_key();
let fixture = start_with_key(&key, Access::Read).await;
let session = authenticate(fixture.server.local_addr(), &key, "git")
.await
.expect("connect")
.expect("authenticate");
let huge = "version=2:".repeat(1024);
assert!(huge.len() > gunnar_ssh::server::MAX_ENV_VALUE);
let (env, _) = exec_with_env(&session, &[("GIT_PROTOCOL", &huge)], UPLOAD_PACK)
.await
.expect("exec");
assert_eq!(env, vec![EnvOutcome::Refused], "{env:?}");
assert_eq!(fixture.transport.git_protocols(), vec![None]);
let big = "v".repeat(gunnar_ssh::server::MAX_ENV_VALUE);
let (env, _) = exec_with_env(&session, &[("GIT_PROTOCOL", &big)], UPLOAD_PACK)
.await
.expect("exec");
assert_eq!(env, vec![EnvOutcome::Accepted], "{env:?}");
fixture
.server
.shutdown("test over")
.await
.expect("shutdown");
}
#[test]
fn the_allowlist_holds_exactly_one_name() {
assert_eq!(
gunnar_ssh::ENV_ALLOWLIST.as_slice(),
["GIT_PROTOCOL"],
"gunnar accepts one environment variable; adding another is a security decision"
);
assert_eq!(gunnar_ssh::ENV_GIT_PROTOCOL, "GIT_PROTOCOL");
}
#[test]
fn the_routine_env_refusals_are_the_ones_openssh_sends_unasked() {
for sent_by_every_client in ["LANG", "LC_ALL", "LC_CTYPE", "LC_TIME", "COLORTERM", "TERM"] {
assert!(
gunnar_ssh::env_refusal_is_routine(sent_by_every_client),
"{sent_by_every_client} is sent by stock OpenSSH without being asked, so refusing \
it is not an anomaly and must not warn"
);
}
for worth_waking_someone_for in [
"LD_PRELOAD",
"LD_LIBRARY_PATH",
"PATH",
"GIT_DIR",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_CONFIG",
"RUST_LOG",
"LANGUAGES_I_MADE_UP",
] {
assert!(
!gunnar_ssh::env_refusal_is_routine(worth_waking_someone_for),
"{worth_waking_someone_for} is not something a client sends by accident; refusing \
it must stay at warn"
);
}
assert_eq!(gunnar_ssh::ENV_ALLOWLIST.as_slice(), ["GIT_PROTOCOL"]);
for routine in gunnar_ssh::ENV_ROUTINE {
assert!(
!gunnar_ssh::ENV_ALLOWLIST.contains(&routine),
"{routine} is quieter, not permitted"
);
}
}