Cloud Security
aws
s3
cloud security
misconfiguration
+3 more

AWS S3 Bucket Misconfigurations: How Data Leaks Happen and How to Prevent Them

SCRs Team
March 16, 2026
14 min read
Share

S3 Misconfigurations: Still the #1 Cloud Security Risk

Despite years of warnings, S3 bucket misconfigurations caused over 80% of cloud data breaches in 2025 (Qualys Cloud Security Report). Billions of records — medical data, financial records, credentials — exposed because of simple configuration errors.

High-Profile S3 Breaches

CompanyRecords ExposedRoot Cause
Capital One106 millionSSRF + overpermissive IAM role
TwitchComplete source codeMisconfigured S3 access
Hobby Lobby138GB customer dataPublic bucket
Accenture40,000 passwordsFour public S3 buckets
U.S. Military1.8 billion social media postsUnsecured S3 bucket

Misconfiguration #1: Public Access via ACLs

# ❌ DANGEROUS: Making a bucket publicly readable
aws s3api put-bucket-acl --bucket my-data --acl public-read

# ❌ Even worse: Public write access
aws s3api put-bucket-acl --bucket my-data --acl public-read-write

Detection

# Check if any buckets have public ACLs
aws s3api get-bucket-acl --bucket BUCKET_NAME \
  | jq '.Grants[] | select(.Grantee.URI == "http://acs.amazonaws.com/groups/global/AllUsers")'
  
# List ALL public buckets in your account
for bucket in $(aws s3api list-buckets --query 'Buckets[].Name' --output text); do
  acl=$(aws s3api get-bucket-acl --bucket "$bucket" 2>/dev/null)
  if echo "$acl" | grep -q "AllUsers"; then
    echo "⚠️  PUBLIC: $bucket"
  fi
done

Fix: Block Public Access at Account Level

# ✅ Block ALL public access for the entire AWS account
aws s3control put-public-access-block \
  --account-id $(aws sts get-caller-identity --query Account --output text) \
  --public-access-block-configuration \
    BlockPublicAcls=true,\
    IgnorePublicAcls=true,\
    BlockPublicPolicy=true,\
    RestrictPublicBuckets=true

Misconfiguration #2: Overly Permissive Bucket Policies

// ❌ This policy allows ANYONE to read ALL objects
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::my-bucket/*"
  }]
}
// ✅ Restrict to specific IAM role only
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "AWS": "arn:aws:iam::123456789012:role/AppServerRole"
    },
    "Action": ["s3:GetObject"],
    "Resource": "arn:aws:s3:::my-bucket/public/*"
  }, {
    "Effect": "Deny",
    "Principal": "*",
    "Action": "s3:*",
    "Resource": "arn:aws:s3:::my-bucket/*",
    "Condition": {
      "Bool": { "aws:SecureTransport": "false" }
    }
  }]
}

Misconfiguration #3: No Encryption

# ❌ No server-side encryption — data at rest is unencrypted
aws s3 cp secret.txt s3://my-bucket/

# ✅ Enable default encryption with AWS-managed keys
aws s3api put-bucket-encryption --bucket my-bucket \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms",
        "KMSMasterKeyID": "alias/my-s3-key"
      },
      "BucketKeyEnabled": true
    }]
  }'

Misconfiguration #4: No Versioning or Logging

Without versioning, a single delete command wipes data permanently. Without logging, you can't detect unauthorized access.

# ✅ Enable versioning
aws s3api put-bucket-versioning --bucket my-bucket \
  --versioning-configuration Status=Enabled

# ✅ Enable server access logging
aws s3api put-bucket-logging --bucket my-bucket \
  --bucket-logging-status '{
    "LoggingEnabled": {
      "TargetBucket": "my-access-logs-bucket",
      "TargetPrefix": "s3-logs/my-bucket/"
    }
  }'

# ✅ Enable CloudTrail data events for the bucket
aws cloudtrail put-event-selectors --trail-name my-trail \
  --event-selectors '[{
    "ReadWriteType": "All",
    "DataResources": [{
      "Type": "AWS::S3::Object",
      "Values": ["arn:aws:s3:::my-bucket/"]
    }]
  }]'

Misconfiguration #5: CORS Allowing All Origins

// ❌ Allows any website to read your S3 objects
{
  "CORSRules": [{
    "AllowedOrigins": ["*"],
    "AllowedMethods": ["GET", "PUT", "POST"],
    "AllowedHeaders": ["*"]
  }]
}

// ✅ Restrict to your domains only
{
  "CORSRules": [{
    "AllowedOrigins": ["https://myapp.com", "https://staging.myapp.com"],
    "AllowedMethods": ["GET"],
    "AllowedHeaders": ["Authorization"],
    "MaxAgeSeconds": 3600
  }]
}

Automated Detection with Terraform

# Secure S3 bucket module
resource "aws_s3_bucket" "secure" {
  bucket = "my-secure-bucket"
}

resource "aws_s3_bucket_public_access_block" "secure" {
  bucket = aws_s3_bucket.secure.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "secure" {
  bucket = aws_s3_bucket.secure.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.s3.arn
    }
    bucket_key_enabled = true
  }
}

resource "aws_s3_bucket_versioning" "secure" {
  bucket = aws_s3_bucket.secure.id
  versioning_configuration { status = "Enabled" }
}

S3 Security Checklist

  • Account-level public access block enabled
  • No bucket ACLs granting public access
  • Bucket policies follow least privilege
  • Default encryption enabled (SSE-KMS preferred)
  • Versioning enabled on all data buckets
  • Server access logging or CloudTrail data events enabled
  • CORS restricted to specific origins
  • MFA Delete enabled for critical buckets
  • Lifecycle policies to expire/transition old objects
  • Regular audits with AWS Config rules or ScoutSuite

Advertisement