Back to Checks
aws

S3 bucket policy does not allow cross-account access

s3_bucket_cross_account_access
Service: s3Severity: high
OfficialBuilt-inVerified
Categories:identity-accesstrust-boundaries
Resource Type:AwsS3Bucket

Amazon S3 bucket policies are analyzed for statements that grant cross-account access.

Any policy that names principals outside the owning account (other account IDs or Principal: "*") is treated as cross-account; absence of a policy implies no cross-account grants.

Risk

Cross-account grants can let external principals read, write, or administer the bucket, impacting:

  • Confidentiality: unauthorized object access/exfiltration
  • Integrity: object tampering, policy or encryption changes
  • Availability: deletions, versioning changes, or lockouts causing data loss and downtime

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks s3_bucket_cross_account_access

Recommendation

Enforce least privilege: limit bucket policy Principal to your account or approved org IDs with fixed values; avoid wildcards.

Use role-based cross-account access with scoped permissions when needed. Add defense-in-depth conditions (private networks, TLS), and periodically review for unintended external access.

Remediation

CLI

aws s3api delete-bucket-policy --bucket <example_resource_name>

Terraform

# Terraform: restrict bucket policy to this AWS account only
resource "aws_s3_bucket_policy" "<example_resource_name>" {
  bucket = "<example_resource_name>"
  policy = jsonencode({
    Version   = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { AWS = "arn:aws:iam::<example_resource_id>:root" } # Critical: limits access to the bucket owner's account, preventing cross-account access
      Action    = "s3:*"
      Resource  = [
        "arn:aws:s3:::<example_resource_name>",
        "arn:aws:s3:::<example_resource_name>/*"
      ]
    }]
  })
}

NativeIaC

# CloudFormation: restrict bucket policy to this AWS account only
Resources:
  <example_resource_name>BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: <example_resource_name>
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub arn:aws:iam::${AWS::AccountId}:root  # Critical: limits access to the bucket owner's account, preventing cross-account access
            Action: s3:*
            Resource:
              - !Sub arn:aws:s3:::<example_resource_name>
              - !Sub arn:aws:s3:::<example_resource_name>/*

Other

1In the AWS console, go to S3 > Buckets > select the bucket
2Open the Permissions tab > Bucket policy
3Click Delete bucket policy (or remove all statements) and Save
4If a policy is required, ensure all statements only use your account as Principal (arn:aws:iam::<YOUR_ACCOUNT_ID>:root); remove any other accounts or "*"

Check Code

from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.iam.lib.policy import is_policy_public
from prowler.providers.aws.services.s3.s3_client import s3_client


class s3_bucket_cross_account_access(Check):
    def execute(self):
        findings = []
        trusted_account_ids = s3_client.audit_config.get("trusted_account_ids", [])
        for bucket in s3_client.buckets.values():
            if bucket.policy is None:
                continue
            report = Check_Report_AWS(metadata=self.metadata(), resource=bucket)
            report.status = "PASS"
            report.status_extended = f"S3 Bucket {bucket.name} has a bucket policy but it does not allow cross account access."

            if not bucket.policy:
                report.status = "PASS"
                report.status_extended = (
                    f"S3 Bucket {bucket.name} does not have a bucket policy."
                )
            elif is_policy_public(
                bucket.policy,
                s3_client.audited_account,
                is_cross_account_allowed=False,
                trusted_account_ids=trusted_account_ids,
            ):
                report.status = "FAIL"
                report.status_extended = f"S3 Bucket {bucket.name} has a bucket policy allowing cross account access."

            findings.append(report)

        return findings