Repository navigation
fix(arm): validate attribute mapping on GCP WIF provider reuse - #1673
Conversation
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 35 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
Comment |
|
@coderabbitai full review |
✅ Action performedFull review finished. Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 48 minutes. |
The provider-reuse path in setup-gcp-wif.sh compared an existing provider's attributeCondition against the expected value but never checked attributeMapping. The condition is only meaningful relative to the mapping that feeds it: a pre-existing provider whose condition is byte-identical to expected but whose mapping derives attribute.aws_role from a caller-controlled ARN path segment instead of the real assumed-role name lets an attacker who can create an IAM user with a crafted path impersonate the trusted role, while the script reports success. Extract the AWS and OIDC attribute mappings into EXPECTED_MAPPING (previously duplicated inline at both create-provider call sites) and compare it against the existing provider's mapping on reuse, failing the same way the condition check does on mismatch. gcloud's `value(...)` printer renders attributeMapping as semicolon-joined, key-sorted pairs, not the comma-joined form the --attribute-mapping flag takes as input, so a raw string comparison would false-positive on every correctly configured provider. Added normalize_mapping() to parse both sides into sorted key/value pairs before comparing. Also records that the AWS session-ARN 127-byte budget relies on the role/subject charset staying ASCII (chars == bytes).
The provider-reuse check on setup-gcp-wif.sh ran a single describe with --format='value(aws.accountId,oidc.issuerUri)' and tested the tab-joined result with a suffix glob (OIDC) or prefix glob (AWS). Both are substring tests standing in for equality: an existing OIDC provider whose issuer merely ENDS with the expected one (e.g. an attacker-hosted https://evil.example.com/<expected-issuer>, an ordinary HTTPS URL GCP would fetch /.well-known/openid-configuration from) passed the check, as did an AWS account ID merely STARTING with the expected one. That made the attribute-condition and attribute-mapping checks below it vacuous, since they only bind the identity this check was supposed to have already pinned. Split into two separate describe calls, one per field, each compared with exact !=. Pre-existing in the reuse path added by #1651, not introduced by this PR. This is the fifth substring-standing-in-for- equality defect found in this codebase.
65de3d2 to
235eb7f
Compare
Review and fix record (merged)Two rounds of independent adversarial review; recording both because CodeRabbit's status on the final head read Round 1 — mapping validationThe reuse path validated The comparison is the part that had to be right in both directions:
Round 2 — the issuer check was a substring testFound by adversarial review of this PR, pre-existing rather than introduced here, and it negated the PR's own goal:
The AWS side had the mirror shape: Fix: split the single This was the sixth substring-standing-in-for-equality defect found in this codebase in one session. Cross-PR interaction caught before it shippedThis PR's exact-set |
Closes #1672 (refs #1544)
PR #1651 (merged as
3fc9b5700) hardenedarm/CUDly-CrossSubscription/setup-gcp-wif.sh's provider-reuse path to reject an existing provider whoseattributeConditiondoesn't match what this script would have written. A follow-up adversarial review found that check validates only half of the property it exists to enforce: the condition is only meaningful relative to theattributeMappingthat produces the value it is evaluated against, and the mapping was never compared on reuse.The bypass this closes
A pre-existing provider whose condition is byte-identical to the one this script would write (so the existing check passes) but whose mapping is:
An attacker holding only
iam:CreateUserin the trusted AWS account creates an IAM user with path/assumed-role/CUDly-Execution/.extract('assumed-role/{role_name}/')then yieldsCUDly-Execution, soattribute.aws_rolebecomes exactly the pinned role ARN, satisfying both the condition and the narrowprincipalSetgrant this script installs. Full service-account impersonation, and the script reports "Reusing existing provider (attribute condition matches: ...)" as success.This is the same substring-vs-normalisation bypass class as the Terraform sibling's bug (#1667), reachable here only through a hand-crafted mapping on a provider this script did not itself create (this script's own create path is immune, since it writes the mapping and condition together).
The fix
EXPECTED_MAPPINGis now built once per provider type (previously duplicated inline at both thecreate-awsandcreate-oidccall sites — a drift risk this change also removes) and compared against the existing provider's mapping on reuse, failing the same way the condition check does, naming what differed.gcloud'svalue(...)printer rendersattributeMappingas semicolon-joined, key-sorted pairs (confirmed by readinggooglecloudsdk/core/resource/csv_printer.py'sCsvPrinter._AddRecord/ValuePrinter), not the comma-joined form--attribute-mappingtakes as input — a raw string comparison would report every correctly-configured provider as mismatched.normalize_mapping()splits each side on its own pair separator, re-sorts, and compares parsed key/value pairs, so neither that shape difference nor a hypothetical future change to gcloud's key ordering produces a false positive, while an actually-different mapping (including the bypass above) still compares unequal.Also recorded in a comment: the AWS session-ARN 127-byte budget computed a few lines above is only correct because the role-name and OIDC-subject charsets are both ASCII-only (chars == bytes); it would silently mis-budget if that charset were ever widened.
Verification
bash -n— exit 0.shellcheck0.11.0 — exit 0, no findings.normalize_mapping()in isolation: a matching AWS mapping (comma-joined input form vs. semicolon-joined, sorted describe-output form) compares equal; a matching single-pair OIDC mapping compares equal; the exact bypass mapping above compares unequal to expected; gcloud output with keys in the opposite order still compares equal (order independence).gcloudexercising the real script: a provider with a matching condition and matching mapping reaches "Reusing existing provider (attribute condition and mapping match expected values)" and proceeds to grant impersonation. The same setup with the bypass mapping substituted exits 1 withprovider 'cudly-aws' already exists with a different attribute mapping..., printing both the found and expected values.origin/main(cherry-picked cleanly onto current main; scope is exactly this file, this change).Note on branch history
This fix was originally developed against #1651 while that PR was still open (worktree
wt-1544, branchsec/1544-gcp-wif-attribute-condition). By the time it was ready, #1651 had merged (squashed into3fc9b5700), so that branch's push had no PR to land on. This PR is the same commit cherry-picked onto currentmainvia a fresh branch.Update: account/issuer reuse check was also a substring test
A second look at the same provider-reuse path (
65de3d24b) found the check just above the ones this PR added — the one confirming an existing provider is actually the right type (AWS vs. OIDC) before the condition/mapping checks even run — had the identical bypass shape, and it is pre-existing (introduced by #1651, not by this PR).It ran a single
describe --format='value(aws.accountId,oidc.issuerUri)'and tab-joined the two columns, then tested the OIDC side with a suffix glob (!= *"${ISSUER_URI}") and the AWS side with a prefix glob (!= "${AWS_ACCOUNT_ID}"*). Both are substring tests standing in for equality:https://evil.example.com/https://token.actions.githubusercontent.com, an ordinary HTTPS URL from which GCP fetches/.well-known/openid-configuration— passed the suffix test, letting the attacker mint tokens with anysub.123456789012999999against expected123456789012) passed the prefix test.Either way, the type check this PR's condition/mapping checks depend on was vacuous, so a provider under attacker control could pass everything downstream too. This is the fifth substring-standing-in-for-equality defect found in this codebase (after #1667's Terraform sibling, and others in the same review sweep).
Fixed by splitting into two separate
describecalls —value(aws.accountId)andvalue(oidc.issuerUri)— each compared with exact!=, mirroring the pattern the condition/mapping checks already use.Verified against three hostile OIDC issuer shapes (path-embedded, query-string, and concatenated-host, all ending in the legitimate issuer string) plus a digit-appended AWS account ID: all four are accepted by the pre-fix code and rejected post-fix, while a byte-identical legitimate provider is accepted both before and after in both the AWS and OIDC branches.
bash -nandshellcheck0.11.0 both exit 0 on the updated file.