← All cybersecurity posts

S3 Bucket Security: Preventing Public Data Exposure

Exposed S3 buckets have leaked millions of records over the years: customer files, database backups, logs and internal documents. AWS has made buckets private by default, but exposures still happen through policies, ACLs, pre-signed URLs and application logic.

How buckets become exposed

  • Bucket policies granting s3:GetObject to "Principal": "*" for a public website, then sensitive files are added to the same bucket.
  • ACLs set to public-read on objects, in older accounts or with ACLs re-enabled.
  • Listable buckets, where s3:ListBucket is public, letting anyone enumerate every key.
  • Writable buckets, letting attackers upload malware or replace JavaScript files served to your users.
  • Long-lived pre-signed URLs shared in emails or logs.
  • Predictable object keys combined with public access, such as invoices/1001.pdf.
  • Application bugs, where the app issues pre-signed URLs for any key the user requests.

Settings that prevent exposure

  1. S3 Block Public Access, enabled at the account level, overrides public policies and ACLs.
  2. Object Ownership set to "Bucket owner enforced", which disables ACLs entirely.
  3. Default encryption with SSE-S3 or SSE-KMS.
  4. Separate buckets for public assets and private data.
  5. CloudFront with origin access control for public content, so the bucket itself stays private.
  6. Versioning and Object Lock for important data, protecting against deletion and ransomware.

Pre-signed URLs done right

Pre-signed URLs are the right way to give users temporary access to private files, if used carefully:

  • Short expiry: minutes, not days.
  • Authorise first: only generate a URL after checking the user owns the object.
  • Generate per object, with the key decided by the server, not the client.
  • For uploads, restrict content type and size, and use a server-chosen key.
// Check ownership before signing
const file = await db.file.findFirst({ where: { id, orgId: session.orgId } });
if (!file) return notFound();
const url = await getSignedUrl(s3, new GetObjectCommand({ Bucket, Key: file.key }), { expiresIn: 300 });

Auditing your buckets

From the inside:

  • Check Block Public Access at account and bucket level.
  • Review bucket policies for wildcard principals and cross-account access.
  • Use IAM Access Analyzer for S3 to find buckets shared outside your account.
  • Enable server access logging or CloudTrail data events for sensitive buckets.

From the outside:

  • Check whether bucket names used by your apps and DNS records are listable or readable anonymously.
  • Look for bucket URLs in your frontend code and test them unauthenticated.
  • Check for dangling DNS records pointing to deleted buckets, a subdomain takeover risk.

Testing S3 exposure

SecurityInfinity discovers buckets referenced by your domains and applications and tests them for anonymous listing, reading and writing, plus pre-signed URL misuse in your app. Start free.

Frequently asked questions

Are new S3 buckets public by default?

No. New buckets have Block Public Access enabled and ACLs disabled by default. Exposures come from later changes or older configurations.

Is S3 encryption enough to protect data?

Encryption at rest protects against certain threats, but if a bucket is public, S3 decrypts objects for anyone who requests them. Access control is what matters.

How do I host a public website on S3 safely?

Use a dedicated bucket for public files only, ideally behind CloudFront with origin access control, and never mix in private data.

Find what attackers can see, before they do.

AI pentesting agents test your web app, API and cloud, and every finding comes with evidence and a fix. First report in about 10 minutes.

Stay ahead of attackers.

New vulnerabilities, breach write-ups and practical fixes. A few emails a month.