diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index a2ad725a4c..aaef4f9240 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -6930,7 +6930,7 @@ pub async fn sandbox_policy_set( "{} Policy unchanged (version {}, hash: {})", "·".dimmed(), resp.version, - &resp.policy_hash[..12] + short_hash(&resp.policy_hash) ); return Ok(()); } @@ -6939,7 +6939,7 @@ pub async fn sandbox_policy_set( "{} Policy version {} submitted (hash: {})", "✓".green().bold(), resp.version, - &resp.policy_hash[..12] + short_hash(&resp.policy_hash) ); if !wait { @@ -7638,7 +7638,13 @@ fn print_policy_revision_table(revisions: &[openshell_core::proto::SandboxPolicy &rev.policy_hash }; let error_short = if rev.load_error.len() > 40 { - format!("{}...", &rev.load_error[..40]) + // Back off to a char boundary: byte-index slicing panics on + // multi-byte UTF-8 in server-supplied error messages. + let mut end = 40; + while !rev.load_error.is_char_boundary(end) { + end -= 1; + } + format!("{}...", &rev.load_error[..end]) } else { rev.load_error.clone() }; diff --git a/crates/openshell-sandbox/src/mechanistic_mapper.rs b/crates/openshell-sandbox/src/mechanistic_mapper.rs index 8ee2fc37f9..f49004d895 100644 --- a/crates/openshell-sandbox/src/mechanistic_mapper.rs +++ b/crates/openshell-sandbox/src/mechanistic_mapper.rs @@ -308,7 +308,8 @@ fn generate_security_notes(host: &str, port: u16, is_ssrf: bool) -> String { } // High port numbers may indicate ephemeral services. - if port > 49152 { + // The IANA dynamic/private (ephemeral) range is 49152-65535 inclusive. + if port >= 49152 { notes.push(format!( "Port {port} is in the ephemeral range — \ this may be a temporary service." diff --git a/crates/openshell-server/src/grpc/validation.rs b/crates/openshell-server/src/grpc/validation.rs index 2f0ad8d139..10ce121bfe 100644 --- a/crates/openshell-server/src/grpc/validation.rs +++ b/crates/openshell-server/src/grpc/validation.rs @@ -494,7 +494,7 @@ pub(super) fn validate_label_key(key: &str) -> Result<(), Status> { // Name must contain only alphanumeric, hyphens, underscores, and dots if !name .chars() - .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.') { return Err(Status::invalid_argument(format!( "label key name segment contains invalid characters (must be alphanumeric, '-', '_', or '.'): '{key}'" @@ -504,12 +504,12 @@ pub(super) fn validate_label_key(key: &str) -> Result<(), Status> { // Name must start and end with alphanumeric let first = name.chars().next().unwrap(); // safe: we checked !is_empty() let last = name.chars().last().unwrap(); - if !first.is_alphanumeric() { + if !first.is_ascii_alphanumeric() { return Err(Status::invalid_argument(format!( "label key name segment must start with alphanumeric character: '{key}'" ))); } - if !last.is_alphanumeric() { + if !last.is_ascii_alphanumeric() { return Err(Status::invalid_argument(format!( "label key name segment must end with alphanumeric character: '{key}'" ))); @@ -585,7 +585,7 @@ pub(super) fn validate_label_value(value: &str) -> Result<(), Status> { // Must contain only alphanumeric, hyphens, underscores, and dots if !value .chars() - .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.') { return Err(Status::invalid_argument(format!( "label value contains invalid characters (must be alphanumeric, '-', '_', or '.'): '{value}'" @@ -595,12 +595,12 @@ pub(super) fn validate_label_value(value: &str) -> Result<(), Status> { // Must start and end with alphanumeric let first = value.chars().next().unwrap(); // safe: we checked !is_empty() let last = value.chars().last().unwrap(); - if !first.is_alphanumeric() { + if !first.is_ascii_alphanumeric() { return Err(Status::invalid_argument(format!( "label value must start with alphanumeric character: '{value}'" ))); } - if !last.is_alphanumeric() { + if !last.is_ascii_alphanumeric() { return Err(Status::invalid_argument(format!( "label value must end with alphanumeric character: '{value}'" ))); @@ -1480,6 +1480,17 @@ mod tests { assert!(err.message().contains("invalid characters")); } + #[test] + fn validate_label_key_rejects_unicode_characters() { + // The Kubernetes label spec is ASCII-only; Unicode alphanumerics + // must not pass gateway validation. + let err = validate_label_key("日本語").unwrap_err(); + assert_eq!(err.code(), Code::InvalidArgument); + + let err = validate_label_key("café").unwrap_err(); + assert_eq!(err.code(), Code::InvalidArgument); + } + // ---- Label value validation ---- #[test] @@ -1547,6 +1558,14 @@ mod tests { assert!(err.message().contains("invalid characters")); } + #[test] + fn validate_label_value_rejects_unicode_characters() { + // The Kubernetes label spec is ASCII-only; Unicode alphanumerics + // must not pass gateway validation. + let err = validate_label_value("café").unwrap_err(); + assert_eq!(err.code(), Code::InvalidArgument); + } + // ---- Label selector validation ---- #[test]