S3 bucket policy confused-deputy patterns
TL;DR: S3 bucket policies are evaluated against the requesting principal and the calling AWS service. When a bucket policy uses a
Conditionblock to restrict byaws:SourceAccount,aws:SourceArn,aws:PrincipalOrgID, oraws:SourceVpc, mistakes in the condition shape (missing keys, wrong key, mixing principal and source) create confused-deputy paths where one AWS service can act on the bucket on behalf of an unintended principal. Multi-tenant SaaS on AWS has repeatedly shipped variants of this bug. Companion to cloud-iam-misconfig-patterns and aws-imds-ssrf-pivot.
The confused-deputy concept
“Confused deputy” is a 1980s security term — a privileged service does work on behalf of a caller, and the privilege of the service is conflated with the privilege of the caller. The fix is to require the caller to identify themselves in the request, and the policy enforces both identities.
For S3, the typical confused deputy is:
- A trusted AWS service (CloudFront, CloudTrail, AWS Config, SNS, Lambda, AWS Backup) writes to the bucket.
- The bucket policy allows the service principal (e.g.,
cloudfront.amazonaws.com). - Without a source-account / source-arn condition, any AWS customer’s CloudFront distribution can be configured to write into the bucket — because all CloudFront distributions present the same service principal.
Pattern 1 — CloudTrail / Config / VPC Flow Logs without aws:SourceAccount
Documentation snippets historically showed:
1
2
3
4
5
6
{
"Effect": "Allow",
"Principal": {"Service": "cloudtrail.amazonaws.com"},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::audit-bucket/AWSLogs/*"
}
Any AWS account’s CloudTrail can write to your bucket — fine if you only care about ingestion, but if a downstream consumer trusts “logs in this bucket are mine”, you’ve been confused.
Fixed shape:
1
2
3
4
5
6
{
"Condition": {
"StringEquals": {"aws:SourceAccount": "1234567890"},
"ArnLike": {"aws:SourceArn": "arn:aws:cloudtrail:us-east-1:1234567890:trail/*"}
}
}
Pattern 2 — Lambda role chain without aws:SourceArn
A Lambda role allowed to read a bucket — but the bucket policy allows lambda.amazonaws.com to PUT objects without source-arn restriction. Any Lambda in any account can write.
Pattern 3 — Cross-account read with aws:PrincipalOrgID typo
1
2
3
4
5
{
"Condition": {
"StringEquals": {"aws:PrincipalOrgId": "o-abc1234"}
}
}
Note the lowercase Id. The condition key is aws:PrincipalOrgID (uppercase ID). The wrong-cased key isn’t an error; it’s silently ignored. Without the condition the policy effectively allows everyone.
Same pattern with aws:sourceAccount vs aws:SourceAccount and other case-sensitive keys.
Pattern 4 — Public read with bucket-key encryption
When a bucket uses SSE-KMS with bucket keys, the KMS key policy can be confused-deputy’d. A KMS key policy that allows S3 service principal to use the key — without a source-account or via-S3 condition — lets S3-from-any-account use the key (encrypting / decrypting objects on behalf of arbitrary callers).
Pattern 5 — Pre-signed URL trust assumption
Pre-signed URLs are generated by callers with valid credentials. If your IAM role has s3:GetObject and you give it to a Lambda exposed via API Gateway, an attacker can call the Lambda to mint pre-signed URLs for any object the role can access. The “deputy” is your Lambda.
Defence: filter object keys the Lambda is allowed to sign for; don’t expose a sign-anything wrapper.
Recon approach
Audit S3 bucket policies in the target account:
aws s3api list-buckets- For each:
aws s3api get-bucket-policy --bucket NAME - Parse the policy JSON; look for:
- Service principals without
aws:SourceAccount/aws:SourceArn. - Wildcard principals (
"Principal": "*"). - Mis-cased condition keys.
aws:PrincipalOrgIDwithout anaws:PrincipalArnconstraint when broad.
- Service principals without
Public tools: cloudsplaining, PMapper, Prowler, policy_sentry.
Workflow to study
- Create two AWS accounts.
- In account A, create a bucket and apply a
cloudtrail.amazonaws.compolicy withoutaws:SourceAccount. - In account B, configure CloudTrail to deliver to account A’s bucket. Observe success.
- Add
aws:SourceAccountcondition to account A’s bucket. Observe account B’s CloudTrail fails to deliver.
Defensive baseline
- Always pair service-principal allowances with
aws:SourceAccount(andaws:SourceArnwhen known). - Audit policy keys with a case-sensitive validator.
- Use
aws:PrincipalOrgIDto restrict cross-account by org. - For KMS bucket keys, restrict via
kms:ViaServicecondition.
Related cloud confused-deputy patterns
- EventBridge cross-account targets — similar shape.
- SNS / SQS subscription — service principals on policies.
- Cognito identity pool federation — confused-deputy across federated identities.
- AssumeRoleWithWebIdentity flows — confused-deputy at the federation layer.
The class is “service principal allowed to act on a resource without identifying the upstream caller”. Audit every such policy.