aws-s3-exfiltrate-bulk-get
s3:ListBucket + s3:GetObject on a bucket enables enumeration and download of all objects - a complete bucket exfiltration.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
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 — 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."