Skip to content

Commit a27eebf

Browse files
authored
fix(sts): validate RoleSessionName against AWS pattern (reject XML injection) (#2252)
2 parents 6b60ff1 + 8115a30 commit a27eebf

2 files changed

Lines changed: 41 additions & 0 deletions

File tree

crates/fakecloud-iam/src/sts_service/assume.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ impl StsService {
2121
)
2222
})?;
2323
validate_string_length("roleSessionName", role_session_name, 2, 64)?;
24+
validate_session_name(role_session_name)?;
2425

2526
// Validate optional DurationSeconds (used below for expiration)
2627
if let Some(ds) = req.query_params.get("DurationSeconds") {
@@ -305,6 +306,7 @@ impl StsService {
305306
)
306307
})?;
307308
validate_string_length("roleSessionName", role_session_name, 2, 64)?;
309+
validate_session_name(role_session_name)?;
308310

309311
// WebIdentityToken is required
310312
let web_identity_token = req.query_params.get("WebIdentityToken").ok_or_else(|| {
@@ -657,6 +659,9 @@ impl StsService {
657659
// the issuer/audience claims used for trust-policy enforcement.
658660
let role_session_name =
659661
extract_saml_session_name(saml_assertion).unwrap_or_else(|| "saml-session".to_string());
662+
// The SAML-derived session name is attacker-influenced; reject any that
663+
// breaks the AWS pattern rather than injecting it raw into the response XML.
664+
validate_session_name(&role_session_name)?;
660665
let saml_claims = extract_saml_claims(saml_assertion);
661666

662667
let partition = partition_for_region(&req.region);

crates/fakecloud-iam/src/sts_service/mod.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,28 @@ fn format_expiration(ts: DateTime<Utc>) -> String {
5454
ts.format("%Y-%m-%dT%H:%M:%SZ").to_string()
5555
}
5656

57+
/// Enforce the AWS `RoleSessionName` pattern `[\w+=,.@-]*`. Without this, a
58+
/// name containing `<`, `>` or `&` is interpolated raw into the AssumedRoleId /
59+
/// assumed-role ARN in the XML response, producing malformed (and, for the
60+
/// attacker-controlled SAML session name, injectable) XML that SDK parsers
61+
/// reject. AWS rejects such input up front with a ValidationError.
62+
fn validate_session_name(name: &str) -> Result<(), AwsServiceError> {
63+
if name
64+
.chars()
65+
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '+' | '=' | ',' | '.' | '@' | '-'))
66+
{
67+
return Ok(());
68+
}
69+
Err(AwsServiceError::aws_error(
70+
StatusCode::BAD_REQUEST,
71+
"ValidationError",
72+
format!(
73+
"1 validation error detected: Value '{name}' at 'roleSessionName' failed to satisfy \
74+
constraint: Member must satisfy regular expression pattern: [\\w+=,.@-]*"
75+
),
76+
))
77+
}
78+
5779
/// Test-only wrapper around [`compute_expiration_at`] used by the existing
5880
/// duration unit tests.
5981
#[cfg(test)]
@@ -589,6 +611,20 @@ fn extract_saml_session_name(saml_b64: &str) -> Option<String> {
589611
mod tests {
590612
use super::*;
591613

614+
#[test]
615+
fn session_name_pattern_accepts_valid_and_rejects_xml_metacharacters() {
616+
// AWS-legal characters pass.
617+
for ok in ["testuser", "role.session-1", "a+b=c,d.e@f", "user_123"] {
618+
assert!(validate_session_name(ok).is_ok(), "should accept {ok}");
619+
}
620+
// XML metacharacters (and anything else off-pattern) are rejected with
621+
// ValidationError rather than injected raw into the response XML.
622+
for bad in ["a<b", "a>b", "a&b", "a\"b", "a b", "a/b"] {
623+
let err = validate_session_name(bad).unwrap_err();
624+
assert_eq!(err.code(), "ValidationError", "should reject {bad:?}");
625+
}
626+
}
627+
592628
#[test]
593629
fn test_partition_for_region() {
594630
assert_eq!(partition_for_region("us-east-1"), "aws");

0 commit comments

Comments
 (0)