From 4c876a201b83185254abb022adc9615e64a0f4fc Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 27 Aug 2026 12:49:59 +0900 Subject: [PATCH] fix: validate projected resource keys canonically --- .../src/feature/builtin/resource_projection.rs | 12 +++++++++++- crates/worker/src/feature/builtin/ticket.rs | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/worker/src/feature/builtin/resource_projection.rs b/crates/worker/src/feature/builtin/resource_projection.rs index 96361787..0879f415 100644 --- a/crates/worker/src/feature/builtin/resource_projection.rs +++ b/crates/worker/src/feature/builtin/resource_projection.rs @@ -649,7 +649,10 @@ fn human_ref(object: &Map, key: &str, prefix: &str) -> Result Result { - if value.starts_with(prefix) && value.len() > prefix.len() { + let valid = value.strip_prefix(prefix).is_some_and(|sequence| { + !sequence.is_empty() && sequence.bytes().all(|byte| byte.is_ascii_digit()) + }); + if valid { Ok(value) } else { Err(format!("required {prefix} human key is unavailable")) @@ -768,6 +771,13 @@ mod tests { assert!(!objective_json.contains("00001TICKETINTERNAL")); } + #[test] + fn human_resource_projection_rejects_noncanonical_keys() { + for (key, prefix) in [("T-key", "T-"), ("O-", "O-"), ("W-1x", "W-")] { + assert!(validate_human_ref(key.to_string(), prefix).is_err()); + } + } + #[test] fn ticket_projection_fails_closed_without_worker_resource_key() { let error = project_worker(&json!({"worker_resource_key": null})) diff --git a/crates/worker/src/feature/builtin/ticket.rs b/crates/worker/src/feature/builtin/ticket.rs index c9fe7a1f..61864167 100644 --- a/crates/worker/src/feature/builtin/ticket.rs +++ b/crates/worker/src/feature/builtin/ticket.rs @@ -778,7 +778,7 @@ impl WorkspaceHttpTicketBackend { if let Some(resource_key) = object .get("resource_key") .and_then(Value::as_str) - .filter(|key| key.starts_with("T-")) + .filter(|key| is_canonical_ticket_resource_key(key)) .map(ToOwned::to_owned) && object.contains_key("id") {