aws-s3-exfiltrate-bulk-get

s3:ListBucket + s3:GetObject on a bucket enables enumeration and download of all objects - a complete bucket exfiltration.

derived aws emits CanExfiltrate

match

A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.

{'principal': None} CanReadData {'bucket': None}

where

node_type(?bucket) == ObjectStorage ?bucket.provider_type == 'AWS::S3::Bucket' ?principal has EFFECTIVE s3:ListBucket on ?bucket ARN (or bucket policy grants ListBucket)

emit

source typeIdentity
target typeObjectStorage
source?principal
target?bucket
permissionss3:GetObject s3:ListBucket
conditionsiam_permission resource_policy scp_or_org_policy
state logicACTIVE when both CanReadData(?principal, ?bucket) is ACTIVE AND s3:ListBucket is EFFECTIVE on the bucket ARN. CONDITIONAL(key_permission) when the contributing CanReadData is CONDITIONAL(key_permission) (SSE-KMS encrypted — downloaded objects are ciphertext until decrypt is confirmed). BLOCKED when CanReadData is BLOCKED. Note: even without s3:ListBucket, an attacker with a known object key list can still exfiltrate individual objects — treat as CONDITIONAL (iam_permission) with a note that bulk enumeration requires ListBucket.

Narrative

{principal.name} has s3:GetObject + s3:ListBucket on {bucket.name} and can enumerate and download all objects - a complete bucket exfiltration path.

Raw rule rules/derived/aws/s3.yaml

id: aws-s3-exfiltrate-bulk-get
emits: CanExfiltrate
description: "s3:ListBucket + s3:GetObject on a bucket enables enumeration and download of all objects\
  \ \u2014 a complete bucket exfiltration."
match:
- - principal: null
  - CanReadData
  - bucket: null
where:
- node_type(?bucket) == ObjectStorage
- ?bucket.provider_type == 'AWS::S3::Bucket'
- ?principal has EFFECTIVE s3:ListBucket on ?bucket ARN (or bucket policy grants ListBucket)
emit:
  source_type: Identity
  target_type: ObjectStorage
  source: ?principal
  target: ?bucket
  permissions:
  - s3:GetObject
  - s3:ListBucket
  conditions:
  - iam_permission
  - resource_policy
  - scp_or_org_policy
  state_logic: "ACTIVE when both CanReadData(?principal, ?bucket) is ACTIVE AND s3:ListBucket is EFFECTIVE\
    \ on the bucket ARN. CONDITIONAL(key_permission) when the contributing CanReadData is CONDITIONAL(key_permission)\
    \ (SSE-KMS encrypted \u2014 downloaded objects are ciphertext until decrypt is confirmed). BLOCKED\
    \ when CanReadData is BLOCKED. Note: even without s3:ListBucket, an attacker with a known object key\
    \ list can still exfiltrate individual objects \u2014 treat as CONDITIONAL (iam_permission) with a\
    \ note that bulk enumeration requires ListBucket."
  confidence: min(contributing_confidences) * 0.90
  derived_from:
  - ?principal CanReadData ?bucket (s3:GetObject)
  - ?principal HasPermission ?bucket (s3:ListBucket effective)
  false_positive_note: "s3:ListBucket is a BUCKET-level action (Resource: arn:aws:s3:::bucket-name), while\
    \ s3:GetObject is an OBJECT-level action (Resource: arn:aws:s3:::bucket-name/*). A policy granting\
    \ s3:GetObject on objects but NOT s3:ListBucket on the bucket still allows GetObject of known-key\
    \ objects, but not enumeration. Emit CanExfiltrate ACTIVE only when both permissions are confirmed;\
    \ otherwise CONDITIONAL(iam_permission) for the list-less path. SSE-KMS encrypted objects: the caller\
    \ downloads ciphertext; exfiltration of plaintext is CONDITIONAL on kms:Decrypt (same gate as CanReadData\
    \ rule 2). S3 Glacier archived objects require a Restore first (s3:RestoreObject); downloading an\
    \ archived object without a restore attempt yields a 403 \u2014 CONDITIONAL for that object tier."
  narrative: "{principal.name} has s3:GetObject + s3:ListBucket on {bucket.name} and can enumerate and\
    \ download all objects \u2014 a complete bucket exfiltration path."
move · open · esc close