diff --git a/hive-forge/src/client.rs b/hive-forge/src/client.rs index 2ec8b41d..1ae86812 100644 --- a/hive-forge/src/client.rs +++ b/hive-forge/src/client.rs @@ -241,13 +241,24 @@ pub fn index(n: u64) -> Result { /// the exact same two files `dashboard/extra_forges.rs` writes, so the /// read side can't drift from the write side. An unknown label (either /// file missing) is a clear error listing the labels actually found in -/// the state dir, not a raw file-not-found. +/// the state dir, not a raw file-not-found. A label outside the plain +/// identifier charset the dashboard accepts (e.g. a typo containing +/// `/` or `..`) is rejected up front with the same charset spelled out, +/// rather than silently building a nonsense/traversing path and +/// surfacing a confusing file error later. fn resolve_credentials(forge_label: Option<&str>) -> Result<(String, String)> { let Some(label) = forge_label else { let base = std::env::var("HIVE_FORGE_URL").unwrap_or_else(|_| DEFAULT_URL.to_owned()); let token = read_token().context("read forge-token")?; return Ok((base, token)); }; + if !is_plain_ident(label) { + bail!( + "hive-forge: invalid --forge label {label:?} — must be lowercase \ + letters, digits, and hyphens only (same rule the dashboard's \ + FORGES tab enforces)" + ); + } let dir = state_dir(); let token_path = dir.join(format!("forge-{label}-token")); @@ -270,6 +281,19 @@ fn resolve_credentials(forge_label: Option<&str>) -> Result<(String, String)> { bail!("hive-forge: no such forge {label:?} — provisioned forges: {known}"); } +/// Plain-identifier check matching `dashboard/extra_forges.rs`'s +/// `is_plain_ident` (itself matching hive-priv's `validate_name_chars`) +/// — lowercase ascii + digits + hyphens only. Rejecting anything else +/// up front (rather than just letting a weird label fail to resolve a +/// file) turns a confusing "no such forge" surprise into a precise +/// "that's not a valid label" one, and incidentally means a label like +/// `../../etc` can't be used to build a path outside the state dir. +fn is_plain_ident(s: &str) -> bool { + !s.is_empty() + && s.chars() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') +} + /// Sidecar shape `dashboard/extra_forges.rs` writes alongside each /// `forge-