From 528c3278b7fbc5b3928af0da163bcf462058d25a Mon Sep 17 00:00:00 2001 From: Alexander Kharkevich Date: Wed, 3 Jun 2026 23:39:50 -0400 Subject: [PATCH] fix(iam): allow colons and dots in STS claim policy names (#3164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `is_safe_claim_policy_name` rejected any character other than `[a-zA-Z0-9_-]`, silently dropping policy names containing colons. This breaks Kubernetes workload identity where `claim_name=sub` resolves to `system:serviceaccount::` — a valid policy name that can be created via the admin API but is then unreachable during STS session authorization. Add `:` and `.` to the allowed character set. These characters are: - Used in K8s service account `sub` claims (colons) - Used in Java/DNS-style group names from OIDC providers (dots) - Already accepted by the `add-canned-policy` admin API endpoint Require at least one alphanumeric character to prevent meaningless names (`.`, `..`, `-`, `_`, `:`, etc.) from resolving. Still rejected: `/`, `\`, whitespace, `$`, `;`, `{`, `}` and other chars that could enable path traversal or injection. Signed-off-by: Alexander Kharkevich Co-authored-by: houseme Co-authored-by: 安正超 Co-authored-by: loverustfs Co-authored-by: GatewayJ <835269233@qq.com> --- crates/iam/src/sys.rs | 81 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/crates/iam/src/sys.rs b/crates/iam/src/sys.rs index 84c2d4a0d..05c7cb69f 100644 --- a/crates/iam/src/sys.rs +++ b/crates/iam/src/sys.rs @@ -834,8 +834,27 @@ impl IamSys { self.store.policy_db_get(name, groups).await } + /// Check whether a policy name from a JWT claim is safe to resolve against the IAM store. + /// + /// Allowed characters: `[a-zA-Z0-9_:.-]` + /// - Colons: K8s service account `sub` claims (`system:serviceaccount:ns:sa`) + /// - Dots: OIDC group names from providers like Okta/Entra (`org.team.role`) + /// + /// Requirements: + /// - At least one ASCII alphanumeric character (prevents meaningless names + /// like `.`, `..`, `-`, `___`, or `:.:`). + /// - No characters outside the allowed set (helps mitigate path traversal + /// and injection when names are used in storage keys or log output). fn is_safe_claim_policy_name(policy: &str) -> bool { - !policy.is_empty() && policy.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + let mut has_alnum = false; + for c in policy.bytes() { + if c.is_ascii_alphanumeric() { + has_alnum = true; + } else if !matches!(c, b'_' | b'-' | b':' | b'.') { + return false; + } + } + has_alnum } // JWT policy claims carry canned policy names only; policy documents are resolved by IAM store. @@ -2667,4 +2686,64 @@ mod tests { policy_info.policy ); } + + #[test] + fn test_is_safe_claim_policy_name_allows_k8s_sa_sub() { + assert!(IamSys::::is_safe_claim_policy_name( + "system:serviceaccount:my-namespace:my-sa" + )); + } + + #[test] + fn test_is_safe_claim_policy_name_allows_dotted_names() { + assert!(IamSys::::is_safe_claim_policy_name("org.team.policy-name")); + } + + #[test] + fn test_is_safe_claim_policy_name_allows_simple_names() { + assert!(IamSys::::is_safe_claim_policy_name("readwrite")); + assert!(IamSys::::is_safe_claim_policy_name("my-custom_policy")); + assert!(IamSys::::is_safe_claim_policy_name("Policy123")); + } + + #[test] + fn test_is_safe_claim_policy_name_rejects_empty() { + assert!(!IamSys::::is_safe_claim_policy_name("")); + } + + #[test] + fn test_is_safe_claim_policy_name_rejects_path_traversal() { + assert!(!IamSys::::is_safe_claim_policy_name("../etc/passwd")); + assert!(!IamSys::::is_safe_claim_policy_name("policy/name")); + assert!(!IamSys::::is_safe_claim_policy_name("policy\\name")); + assert!(!IamSys::::is_safe_claim_policy_name("..\\etc\\passwd")); + } + + #[test] + fn test_is_safe_claim_policy_name_rejects_whitespace() { + assert!(!IamSys::::is_safe_claim_policy_name("my policy")); + assert!(!IamSys::::is_safe_claim_policy_name("policy\tname")); + } + + #[test] + fn test_is_safe_claim_policy_name_rejects_special_chars() { + assert!(!IamSys::::is_safe_claim_policy_name("policy;drop")); + assert!(!IamSys::::is_safe_claim_policy_name("pol$icy")); + assert!(!IamSys::::is_safe_claim_policy_name("name{bad}")); + } + + #[test] + fn test_is_safe_claim_policy_name_rejects_no_alphanumeric() { + assert!(!IamSys::::is_safe_claim_policy_name(".")); + assert!(!IamSys::::is_safe_claim_policy_name("..")); + assert!(!IamSys::::is_safe_claim_policy_name(":")); + assert!(!IamSys::::is_safe_claim_policy_name("...")); + assert!(!IamSys::::is_safe_claim_policy_name(".:.:")); + assert!(!IamSys::::is_safe_claim_policy_name("-")); + assert!(!IamSys::::is_safe_claim_policy_name("_")); + assert!(!IamSys::::is_safe_claim_policy_name("__")); + assert!(!IamSys::::is_safe_claim_policy_name("---")); + assert!(!IamSys::::is_safe_claim_policy_name("-_-")); + assert!(!IamSys::::is_safe_claim_policy_name("-.:_")); + } }