From 720ac81235d350d802deba3b7de3c12ddf5e05d1 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 14 Jul 2026 18:42:17 +0200 Subject: [PATCH] fix(#2391): reject ".." in credential/snapshot names (path-traversal hardening) Per mara's review: validate_credential_name allowed any [A-Za-z0-9_.-] byte sequence, which permits a literal ".." substring. Not currently exploitable (snapshot_path() embeds the label inside a single format!()'d path component with no "/" in the allowed charset, so there's no directory to traverse into), but it's a landmine for any future caller that builds a path via PathBuf::from(name) directly instead of the current string-embedding. Reject ".." outright in the shared validator, plus a matching client-side check in hivectl for fail-fast UX (hive-priv's copy is still the authoritative one). --- hive-c0re/src/bin/hivectl.rs | 3 +++ hive-priv/src/main.rs | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index f3bbe5f7..b47a6a1c 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -1751,6 +1751,9 @@ async fn subvol_snapshot_create(name: &str, label: String) -> Result<()> { if !label.starts_with("hive-") { bail!("snapshot label {label:?} must start with \"hive-\""); } + if label.contains("..") { + bail!("snapshot label {label:?} must not contain \"..\""); + } let path = hive_c0re::priv_client::snapshot_agent_subvolume(name, &label) .await .with_context(|| format!("snapshot {name} state subvolume (label {label:?})"))?; diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 453108ce..a031cb7b 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -455,6 +455,16 @@ fn validate_credential_name(name: &str) -> Result<()> { { bail!("invalid credential name {name:?}: must be non-empty [A-Za-z0-9_.-]"); } + // `.` is in the allowed charset (systemd credential ids and snapshot + // labels both legitimately use dots), but a bare `..` reads as a + // directory-traversal token to anyone auditing this path — and a + // caller that builds a path via `PathBuf::from(name)` instead of the + // current `format!("...{name}...")` embedding would actually be + // exploitable. Reject it outright rather than relying on every future + // caller getting the embedding right. + if name.contains("..") { + bail!("invalid credential name {name:?}: must not contain \"..\""); + } Ok(()) }