How can I find publicly exposed AWS S3 buckets using Python?
Quick Answer
Use boto3 to list every bucket, then for each one check get_bucket_acl for grants to the AllUsers or AuthenticatedUsers well-known groups, and separately parse get_bucket_policy for any statement with Effect Allow and a wildcard Principal. Both checks are necessary because a bucket can be exposed through either mechanism independently, and checking only one gives a false sense of security.
Detailed Answer
Think of an S3 bucket like a storage room that can be unlocked two completely different ways — a physical spare key hidden under the mat, the ACL, and a separate digital keypad code shared in a group chat, the bucket policy. A security guard who only checks whether the key is under the mat, but never asks whether anyone shared the keypad code, will confidently report the room secure while half the building can still walk in through the door they didn't check.
This script exists because S3 public exposure remains one of the most common and highest-impact cloud misconfigurations, precisely because there are two independent ways a bucket becomes public — legacy Access Control Lists, a per-bucket or per-object permission grant system that predates IAM policies, and modern bucket policies, JSON documents defining who can perform which actions — and a security review that checks only the newer mechanism can completely miss an old ACL grant left over from years ago, or vice versa. AWS's own Block Public Access settings can mitigate both, but this script exists specifically to detect exposure directly rather than assume account-level settings are correctly configured everywhere.
For each bucket, the script first checks get_bucket_acl, iterating the Grants list and checking whether any grantee's URI matches the well-known AWS group URIs for AllUsers, meaning anyone on the internet, authenticated or not, or AuthenticatedUsers, meaning any AWS account holder, not just this account's users, which is a much larger and less obvious exposure than the name implies. Separately, it fetches get_bucket_policy, parses the JSON policy document, and inspects each statement for Effect Allow combined with a Principal of "*" or an AWS wildcard, which grants access to literally anyone. The script wraps both checks in try/except ClientError, since a bucket with no policy at all raises NoSuchBucketPolicy, which is an entirely normal, expected condition, not an error to alert on.
In production, this is typically deployed as a scheduled Lambda function feeding AWS Security Hub or a dedicated security Slack channel, and should run frequently, hourly or on every bucket policy change event via CloudTrail and EventBridge, not just daily, because public exposure is a "seconds matter" class of risk, not a "check it eventually" class. What to monitor beyond the binary public/not-public result: exceptions raised while checking ACLs or policies, since an AccessDenied error while scanning could mean the script's own IAM role lost permissions, which would silently blind the entire security check without anyone noticing unless that failure mode is itself alerted on.
The non-obvious gotcha, and the one that actually broke the original version of this script, is a Python syntax error: an if statement missing its trailing colon fails at parse time, meaning the entire script would refuse to even start — a reminder that in security tooling specifically, a script that silently fails to run at all is far more dangerous than one that runs and reports zero findings, because a cron job that errors out on import or syntax can go unnoticed for a long time if nothing alerts on the automation's own health, leaving buckets unmonitored while everyone assumes the nightly scan is working.