>samit_hota
Back to research
CLOUD SECURITY

S3 Leaks Beyond ACLs: Audit Resource Policies and Access Points

Samit Hota·
#aws#s3#cloud-security#audit

The AWS Console likes to reassure you with green badges claiming “Bucket and objects not public,” but that status indicator is dangerously simplistic. It primarily tracks S3 Block Public Access settings and legacy Access Control Lists (ACLs). In reality, buckets leak through complex resource policies, misconfigured S3 Access Points, and overly broad cross-account IAM trusts that bypass those basic checks entirely.

If your S3 security audit stops at checking bucket ACLs, you’re missing the exact attack surface that modern cloud exposure incidents exploit. Here is how to perform a deep-dive audit of your S3 exposure beyond the surface layer using the AWS CLI and IAM Access Analyzer.

Hunting Down Complex Bucket Policy Leaks

Disabling public ACLs and enabling S3 Block Public Access at the bucket level is good baseline hygiene, but bucket policies can still introduce unexpected access paths if Block Public Access was ever partially turned off or configured with exemptions.

A common anti-pattern is using "Principal": "*" alongside a Condition block that the author assumed secured the bucket, but actually fails to do so. For instance, relying on easily forged request headers like aws:UserAgent or aws:Referer leaves the bucket publicly readable to anyone who crafts a custom HTTP request.

To audit a bucket policy directly, fetch the policy JSON and inspect its statements:

aws s3api get-bucket-policy \
  --bucket target-data-bucket \
  --query Policy \
  --output text | jq .

When evaluating the output, look specifically for these constructs:

  1. Wildcard Principals with Weak Conditions: Look for "Principal": "*" or "Principal": {"AWS": "*"} paired with conditions that don’t enforce strong identity constraints (like aws:PrincipalArn or aws:PrincipalOrgID).
  2. Insecure Use of NotPrincipal: Combining "Effect": "Allow" with "NotPrincipal" grants access to everyone except the listed principal. If an unauthenticated attacker requests an object, they are not the excluded principal, so the policy allows the request.
  3. Missing IP or VPC Enforcements: If a bucket policy is meant to restrict access to internal infrastructure, verify that it enforces aws:SourceVpc or aws:SourceIp using a Deny statement with a StringNotEquals condition rather than a permissive Allow.

Auditing S3 Access Points and Their Independent Policies

S3 Access Points are dedicated network endpoints attached to buckets, each with its own distinct resource policy. A bucket policy might look completely locked down, while an Access Point attached to that same bucket exposes objects to external networks or unauthenticated entities.

Crucially, S3 Access Points maintain their own Block Public Access configurations separate from the underlying bucket.

First, enumerate all Access Points in the account for your target region:

aws s3control list-access-points \
  --account-id 123456789012 \
  --query "AccessPointList[*].[Name, NetworkOrigin, VpcConfiguration.VpcId, Bucket]" \
  --output table

Next, evaluate the Block Public Access settings for each Access Point to ensure public access isn’t explicitly permitted at the endpoint layer:

aws s3control get-public-access-block \
  --account-id 123456789012 \
  --name app-data-access-point

Finally, inspect the Access Point policy itself. Access Point policies behave similarly to bucket policies and can grant read/write access independently:

aws s3control get-access-point-policy \
  --account-id 123456789012 \
  --name app-data-access-point \
  --query Policy \
  --output text | jq .

If an Access Point’s network origin is set to Internet and its policy contains a wildcard principal without aws:PrincipalOrgID or explicit IAM user restrictions, your underlying bucket data is publicly accessible through the Access Point alias ARN.

Uncovering Cross-Account Trust Chain Vulnerabilities

Public exposure isn’t always direct unauthenticated access. A common vector involves granting read access on an S3 bucket to a trusted third-party AWS account (e.g., an analytics vendor or partner account), which then misconfigures its own IAM roles.

If Bucket A grants read permissions to arn:aws:iam::999999999999:root, any IAM entity in account 999999999999 with sufficient permissions can read your objects. If that third-party account has a role with an overly permissive trust policy—allowing "Principal": "*" to assume it—an attacker can assume the vendor’s role and pivot directly into your S3 bucket.

To check cross-account grants in your bucket policy using the AWS CLI:

aws s3api get-bucket-policy \
  --bucket target-data-bucket \
  --output json | jq '.Policy | fromjson | .Statement[] | select(.Principal.AWS != null)'

When cross-account principals are identified:

  • Ensure grants target specific role ARNs (arn:aws:iam::999999999999:role/VendorSpecificRole) rather than account roots (arn:aws:iam::999999999999:root).
  • If external roles assume a role inside your account to read S3 data, verify that the assuming role’s trust policy requires a unique sts:ExternalId.

Automating Surface Detection with IAM Access Analyzer

Manual policy analysis breaks down when managing dozens of AWS accounts and hundreds of buckets. AWS IAM Access Analyzer uses mathematical logic (formal reasoning) to analyze resource policies and detect external exposure across buckets, Access Points, and KMS keys.

To set up an account-level analyzer for S3 using the AWS CLI:

aws accessanalyzer create-analyzer \
  --analyzer-name s3-exposure-check \
  --type ACCOUNT

Once the initial scan completes, list all active findings targeting S3 resources:

aws accessanalyzer list-findings \
  --analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/s3-exposure-check \
  --filter '{"status": {"eq": ["ACTIVE"]}, "resourceType": {"eq": ["AWS::S3::Bucket"]}}' \
  --query "findings[*].[id, resource, action, isPublic]" \
  --output table

Access Analyzer flags findings where isPublic is true (accessible to unauthenticated users or any AWS identity) as well as access granted to external AWS principals outside your Zone of Trust (your AWS Organization).

Integrating list-findings queries into your continuous integration pipelines or security automation scripts gives you a reliable signal of unintended S3 exposure before an external scanner indexes your data.

Want a second set of eyes on your security posture?

Let's talk about where your real exposure is.

Book an advisory call