S3 Bucket Policies: The Missing SourceAccount Check
A bucket policy grants a wildcard AWS service Principal like sns.amazonaws.com or cloudtrail.amazonaws.com write access without pinning aws:SourceAccount, so any AWS account can configure that same service to target the bucket. IAM boundaries never trigger, because the request arrives as a trusted first-party service call, not a cross-account API invocation.
At a glance
- Unsafe setting
- An S3 bucket policy grants an AWS service Principal (e.g. cloudtrail.amazonaws.com) access without an aws:SourceAccount condition scoping it to the owning account.
- Failure trigger
- A different AWS account configures the same trusted service to target the exposed bucket ARN, and the service delivers on their behalf without any cross-account check.
- Blast radius
- Unauthenticated writes accumulate as legitimate service traffic, evading GuardDuty and IAM Access Analyzer while inflating storage costs and poisoning downstream log analysis.
- Recommended control
- Require aws:SourceAccount and aws:SourceArn conditions on every service-principal statement and enforce this with a custom AWS Config rule.
The Trap
Service Principal grants in an S3 bucket policy that omit the aws:SourceAccount condition key, trusting an AWS service by name alone rather than scoping which account’s instance of that service may act.
The Default State
Console wizards for S3 event notifications, CloudTrail delivery, ALB access logging, AWS Config recorder delivery, and SES receipt rule storage generate a Principal block such as {“Service”: “cloudtrail.amazonaws.com”} and often include aws:SourceArn, but a large share of hand-written Terraform modules and older CloudFormation samples predating the 2021 confused-deputy advisory skip aws:SourceAccount entirely. Engineers copying a working policy from one project to another rarely notice the omission, since the bucket still functions correctly for the intended account.
The Blast Radius
The policy trusts the named AWS service, not your account specifically. Any AWS customer can create their own CloudTrail trail, SNS topic, or ALB and configure it to deliver output to your bucket ARN if it becomes known through shared logs, Terraform state leaks, or predictable naming. The service then writes objects on their behalf, and because the calling identity in your bucket’s access logs is the legitimate AWS service endpoint, not an external ARN, GuardDuty and IAM Access Analyzer’s cross-account findings stay silent — Access Analyzer flags foreign account ARNs in Principal blocks, not service principals paired with an unscoped SourceAccount. Downstream SIEM pipelines parsing injected log objects as trusted CloudTrail data compound the damage, and storage costs climb from unauthenticated write volume nobody billed for.
The Lead Mechanic Fix
Pin both aws:SourceAccount and aws:SourceArn on every service-principal statement:n{n “Effect”: “Allow”,n “Principal”: {“Service”: “cloudtrail.amazonaws.com”},n “Action”: “s3:PutObject”,n “Resource”: “arn:aws:s3:::bucket/prefix/*”,n “Condition”: {n “StringEquals”: {“aws:SourceAccount”: “111122223333”},n “ArnLike”: {“aws:SourceArn”: “arn:aws:cloudtrail:eu-west-2:111122223333:trail/trailname”}n }n}nAudit live policies with aws s3api get-bucket-policy –bucket , script a check for any Service principal statement lacking both condition keys, and enforce compliance with a custom AWS Config rule that flags or denies PutBucketPolicy calls where a service Principal omits aws:SourceAccount.