gcp-gcs-enter-project-via-set-iam
A cross-project principal that can set the bucket IAM policy can grant itself objectViewer, read the bucket contents, and enter the bucket's project data boundary - especially when the bucket contains privileged credentials.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
{'principal': None}
CanModifyPolicy {'bucket_policy': None}
{'bucket': None} HasPolicy {'bucket_policy': None}
{'bucket': None} ContainsSecret {'secret': None}
where
?principal.project != ?bucket.project # cross-project caller
?bucket_policy is the
ResourcePolicy of ?bucket
emit
| source type | Identity |
|---|---|
| target type | Project |
| source | ?principal |
| target | <bucket.project> |
| permissions | storage.buckets.setIamPolicy storage.objects.get |
| conditions | iam_permission resource_policy |
| state logic | CONDITIONAL(resource_policy) as the default: CanModifyPolicy(setIamPolicy) is necessary but NOT sufficient — the principal must execute the self-grant (write IAM policy to bind itself to objectViewer) and THEN read the bucket objects. These mandatory intermediate steps are not guaranteed even when setIamPolicy is ACTIVE — org policy or IAM deny could block the self-grant at execution time. ACTIVE only if execution of self-grant is confirmed; POTENTIAL if ContainsSecret is POTENTIAL and the attack path is plausible but not yet confirmed. |
Narrative
{principal.name} (project {projectB.name}) holds storage.buckets.setIamPolicy on bucket {bucket.name} (project {projectA.name}) and can grant itself objectViewer; the bucket is classified as containing secrets - reading its objects may yield credentials for project {projectA.name}.
Raw rule rules/derived/gcp/gcs.yaml
id: gcp-gcs-enter-project-via-set-iam
emits: CanEnterProject
description: "A cross-project principal that can set the bucket IAM policy can grant itself objectViewer,\
\ read the bucket contents, and enter the bucket's project data boundary \u2014 especially when the\
\ bucket contains privileged credentials."
match:
- - principal: null
- CanModifyPolicy
- bucket_policy: null
- - bucket: null
- HasPolicy
- bucket_policy: null
- - bucket: null
- ContainsSecret
- secret: null
where:
- '?principal.project != ?bucket.project # cross-project caller'
- ?bucket_policy is the ResourcePolicy of ?bucket
emit:
source_type: Identity
target_type: Project
source: ?principal
target: <bucket.project>
permissions:
- storage.buckets.setIamPolicy
- storage.objects.get
conditions:
- iam_permission
- resource_policy
state_logic: "CONDITIONAL(resource_policy) as the default: CanModifyPolicy(setIamPolicy) is necessary\
\ but NOT sufficient \u2014 the principal must execute the self-grant (write IAM policy to bind itself\
\ to objectViewer) and THEN read the bucket objects. These mandatory intermediate steps are not guaranteed\
\ even when setIamPolicy is ACTIVE \u2014 org policy or IAM deny could block the self-grant at execution\
\ time. ACTIVE only if execution of self-grant is confirmed; POTENTIAL if ContainsSecret is POTENTIAL\
\ and the attack path is plausible but not yet confirmed."
confidence: min(contributing_confidences) * 0.82
derived_from:
- ?principal CanModifyPolicy ?bucket_policy
- ?bucket ContainsSecret ?secret
false_positive_note: "This rule fires when a CROSS-PROJECT principal can write the bucket's IAM policy\
\ AND the bucket contains classified secrets. If the principal is in the SAME project, the data-boundary\
\ crossing is weaker (intra-project). The CanEnterProject impact is data-plane only (read the bucket\
\ contents); full management-plane control requires resourcemanager.projects.setIamPolicy, a different\
\ edge. The ContainsSecret classification must be at least POTENTIAL \u2014 do not emit for buckets\
\ with no credential indicator. Confidence degrades with heuristic ContainsSecret inputs."
narrative: "{principal.name} (project {projectB.name}) holds storage.buckets.setIamPolicy on bucket\
\ {bucket.name} (project {projectA.name}) and can grant itself objectViewer; the bucket is classified\
\ as containing secrets \u2014 reading its objects may yield credentials for project {projectA.name}."