Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gator-agent

Warning: short_hash still uses &hash[..12], so a server response such as aaaaaaaaaaaé panics when byte 12 splits a UTF-8 character (CWE-248). Make short_hash select the twelfth character boundary using char_indices() and add short/multibyte regression cases; that fixes both new call sites.

);
return Ok(());
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gator-agent

Warning: The revision table still byte-slices the server-supplied policy hash, preserving the same UTF-8 panic (CWE-248). After fixing short_hash, replace this branch with let hash_short = short_hash(&rev.policy_hash);.

// 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()
};
Expand Down
3 changes: 2 additions & 1 deletion crates/openshell-sandbox/src/mechanistic_mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
31 changes: 25 additions & 6 deletions crates/openshell-server/src/grpc/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}'"
Expand All @@ -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}'"
)));
Expand Down Expand Up @@ -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}'"
Expand All @@ -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}'"
)));
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
Loading