fix(iam): allow colons and dots in STS claim policy names (#3164)

`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:<namespace>:<sa-name>` — 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 <alex@mara.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: GatewayJ <835269233@qq.com>
This commit is contained in:
Alexander Kharkevich
2026-06-03 23:39:50 -04:00
committed by GitHub
parent 45e3c01857
commit 528c3278b7
+80 -1
View File
@@ -834,8 +834,27 @@ impl<T: Store> IamSys<T> {
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::<StsTestMockStore>::is_safe_claim_policy_name(
"system:serviceaccount:my-namespace:my-sa"
));
}
#[test]
fn test_is_safe_claim_policy_name_allows_dotted_names() {
assert!(IamSys::<StsTestMockStore>::is_safe_claim_policy_name("org.team.policy-name"));
}
#[test]
fn test_is_safe_claim_policy_name_allows_simple_names() {
assert!(IamSys::<StsTestMockStore>::is_safe_claim_policy_name("readwrite"));
assert!(IamSys::<StsTestMockStore>::is_safe_claim_policy_name("my-custom_policy"));
assert!(IamSys::<StsTestMockStore>::is_safe_claim_policy_name("Policy123"));
}
#[test]
fn test_is_safe_claim_policy_name_rejects_empty() {
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name(""));
}
#[test]
fn test_is_safe_claim_policy_name_rejects_path_traversal() {
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("../etc/passwd"));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("policy/name"));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("policy\\name"));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("..\\etc\\passwd"));
}
#[test]
fn test_is_safe_claim_policy_name_rejects_whitespace() {
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("my policy"));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("policy\tname"));
}
#[test]
fn test_is_safe_claim_policy_name_rejects_special_chars() {
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("policy;drop"));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("pol$icy"));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("name{bad}"));
}
#[test]
fn test_is_safe_claim_policy_name_rejects_no_alphanumeric() {
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("."));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name(".."));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name(":"));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("..."));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name(".:.:"));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("-"));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("_"));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("__"));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("---"));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("-_-"));
assert!(!IamSys::<StsTestMockStore>::is_safe_claim_policy_name("-.:_"));
}
}