Severity: CRITICAL. Like #1544, this is customer-facing: iac/embed.go:9 embeds federation, and internal/api/handler_federation.go:641-655 returns federation/gcp-target as the default bundle for target=gcp, so the zip customers download ships main.tf with a pre-filled tfvars. Same endpoint, same customers, same defect class as #1544 - and more exploitable than the bug #1544 reported, because this path has a working bypass rather than just a missing second gate.
#1544's framing said "the shell path lacks the guard its Terraform sibling enforces". That premise is wrong, and PR #1651 disproves it. The Terraform path is the worse of the two.
(a) Live bypass: raw assertion.arn + substring test - HEADLINE
iac/federation/gcp-target/terraform/main.tf:102 maps the attribute raw:
"attribute.aws_role" = "assertion.arn"
and :109 substring-tests it:
"attribute.aws_role.contains('assumed-role/${var.aws_role_name}/')"
With aws_role_name = "CUDly-Execution", an attacker who has only iam:CreateUser in the customer's AWS account:
- Creates an IAM user with path
/assumed-role/CUDly-Execution/. AWS's documented path regex is (/)|(/[!-~]+/) - every character here is printable ASCII, so the path is legal.
sts:GetCallerIdentity returns arn:aws:iam::<acct>:user/assumed-role/CUDly-Execution/evil.
- The provider's
account_id check passes (same account). contains('assumed-role/CUDly-Execution/') is true - the crafted path supplies the substring the test looks for.
- The pool-wide binding in (b) matches every identity in the pool.
Result: full impersonation of the CUDly GCP service account, which holds compute.commitments.create/update. iam:CreateUser escalated into cross-cloud GCP spend authority.
IAM role paths do not work, because STS drops the path from assumed-role ARNs. It is specifically the IAM user ARN, which retains its path, that defeats the substring test.
The shell script fixed in #1651 is immune, and the reason is the argument for the fix shape: it normalises the assertion down to the role ARN and compares with ==, so a user ARN never equals arn:aws:sts::<acct>:assumed-role/<role> regardless of its path.
(b) Pool-wide impersonation grant
main.tf:162:
member = ... "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.cudly.name}/*"
Byte-identical in shape to the grant removed in #1651. Its inline comment - "wildcard principalSet is intentional; session ARNs include variable session names so exact-match principalSet cannot work" - is precisely the claim #1651 disproves.
Worth noting how far the two customer-facing paths have now diverged: the pool_wide_grants() scan added to setup-gcp-wif.sh in #1651 flags a Terraform-created grant as legacy and refuses to report success. A customer who onboards with Terraform and later runs the shell script is told, correctly, that their setup is unsafe.
(c) No validation on the CEL-interpolated variables
variables.tf:57-67: neither aws_role_name nor oidc_subject has a validation block, and both are interpolated into a CEL string literal. aws_role_name = "x') || true || ('" renders an always-true condition. This is the CEL break-out that validate_principal_value() exists to stop on the shell side.
(d) Stale "Recommended" wording
internal/iacfiles/templates/gcp-wif.tfvars.tmpl ships aws_role_name commented out and labelled "Recommended", while main.tf:139-142 requires it. A downloaded bundle fails terraform apply. Fails closed, but the wording misleads.
Fix direction
Mirror PR #1651:
"attribute.aws_role" = "assertion.arn.contains('assumed-role') ? assertion.arn.extract('{account_arn}assumed-role/') + 'assumed-role/' + assertion.arn.extract('assumed-role/{role_name}/') : assertion.arn"
attribute_condition = "attribute.aws_role == 'arn:aws:sts::${var.aws_account_id}:assumed-role/${var.aws_role_name}'"
member = "principalSet://iam.googleapis.com/${pool.name}/attribute.aws_role/arn:aws:sts::${var.aws_account_id}:assumed-role/${var.aws_role_name}"
plus validation blocks on both string vars rejecting ', ", \, backtick, * and $, and a fix to (d).
This changes attribute_mapping on live deployments, which existing customers federate against, so it needs its own migration note rather than a silent edit.
Related: #1544 (same defect class, shell path, fixed in #1651), #1661 (adjacent gaps in the served CLI template).
Severity: CRITICAL. Like #1544, this is customer-facing:
iac/embed.go:9embedsfederation, andinternal/api/handler_federation.go:641-655returnsfederation/gcp-targetas the default bundle fortarget=gcp, so the zip customers download shipsmain.tfwith a pre-filled tfvars. Same endpoint, same customers, same defect class as #1544 - and more exploitable than the bug #1544 reported, because this path has a working bypass rather than just a missing second gate.#1544's framing said "the shell path lacks the guard its Terraform sibling enforces". That premise is wrong, and PR #1651 disproves it. The Terraform path is the worse of the two.
(a) Live bypass: raw
assertion.arn+ substring test - HEADLINEiac/federation/gcp-target/terraform/main.tf:102maps the attribute raw:and
:109substring-tests it:"attribute.aws_role.contains('assumed-role/${var.aws_role_name}/')"With
aws_role_name = "CUDly-Execution", an attacker who has onlyiam:CreateUserin the customer's AWS account:/assumed-role/CUDly-Execution/. AWS's documented path regex is(/)|(/[!-~]+/)- every character here is printable ASCII, so the path is legal.sts:GetCallerIdentityreturnsarn:aws:iam::<acct>:user/assumed-role/CUDly-Execution/evil.account_idcheck passes (same account).contains('assumed-role/CUDly-Execution/')is true - the crafted path supplies the substring the test looks for.Result: full impersonation of the CUDly GCP service account, which holds
compute.commitments.create/update.iam:CreateUserescalated into cross-cloud GCP spend authority.IAM role paths do not work, because STS drops the path from
assumed-roleARNs. It is specifically the IAM user ARN, which retains its path, that defeats the substring test.The shell script fixed in #1651 is immune, and the reason is the argument for the fix shape: it normalises the assertion down to the role ARN and compares with
==, so a user ARN never equalsarn:aws:sts::<acct>:assumed-role/<role>regardless of its path.(b) Pool-wide impersonation grant
main.tf:162:Byte-identical in shape to the grant removed in #1651. Its inline comment - "wildcard principalSet is intentional; session ARNs include variable session names so exact-match principalSet cannot work" - is precisely the claim #1651 disproves.
Worth noting how far the two customer-facing paths have now diverged: the
pool_wide_grants()scan added tosetup-gcp-wif.shin #1651 flags a Terraform-created grant as legacy and refuses to report success. A customer who onboards with Terraform and later runs the shell script is told, correctly, that their setup is unsafe.(c) No validation on the CEL-interpolated variables
variables.tf:57-67: neitheraws_role_namenoroidc_subjecthas avalidationblock, and both are interpolated into a CEL string literal.aws_role_name = "x') || true || ('"renders an always-true condition. This is the CEL break-out thatvalidate_principal_value()exists to stop on the shell side.(d) Stale "Recommended" wording
internal/iacfiles/templates/gcp-wif.tfvars.tmplshipsaws_role_namecommented out and labelled "Recommended", whilemain.tf:139-142requires it. A downloaded bundle failsterraform apply. Fails closed, but the wording misleads.Fix direction
Mirror PR #1651:
plus
validationblocks on both string vars rejecting',",\, backtick,*and$, and a fix to (d).This changes
attribute_mappingon live deployments, which existing customers federate against, so it needs its own migration note rather than a silent edit.Related: #1544 (same defect class, shell path, fixed in #1651), #1661 (adjacent gaps in the served CLI template).