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.

derived gcp emits CanEnterProject

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 typeIdentity
target typeProject
source?principal
target<bucket.project>
permissionsstorage.buckets.setIamPolicy storage.objects.get
conditionsiam_permission resource_policy
state logicCONDITIONAL(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}."
move · open · esc close