Back to Checks
aws

Spot-check S3 bucket objects for public ACLs

s3_bucket_object_public
Service: s3Severity: low
OfficialBuilt-inVerified
Categories:internet-exposed
Resource Type:AwsS3Bucket

Spot-checks a configurable sample of objects in each S3 bucket and flags any whose ACL grants access to the AllUsers or AuthenticatedUsers groups. This is a sampling-based check, not a comprehensive audit, so public objects outside the sample can be missed. It is disabled by default and must be enabled via the s3_bucket_object_public_enabled configuration flag.

Risk

Public objects can be accessed by anyone on the internet, potentially leaking sensitive data. A bucket can appear private at the bucket-policy level while still containing individual objects with public ACL grants.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks s3_bucket_object_public

Recommendation

For complete coverage, enable the s3_bucket_acl_prohibited check, which enforces the BucketOwnerEnforced Object Ownership setting (AWS's recommended approach since April 2023) and prevents public object ACLs entirely. Use this spot-check as a supplementary tool for manual assessments.

Remediation

CLI

aws s3api put-object-acl --bucket <bucket_name> --key <object_key> --acl private

Check Code

from typing import List

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

# ACL grantee groups that make an object effectively public. AllUsers is anyone on
# the internet; AuthenticatedUsers is any authenticated AWS principal (any account).
PUBLIC_ACL_URIS = {
    "http://acs.amazonaws.com/groups/global/AllUsers",
    "http://acs.amazonaws.com/groups/global/AuthenticatedUsers",
}


class s3_bucket_object_public(Check):
    """Spot-check a sample of S3 bucket objects for public ACL grants."""

    def execute(self) -> List[Check_Report_AWS]:
        """Evaluate sampled object ACLs for AllUsers/AuthenticatedUsers grants.

        Returns:
            List[Check_Report_AWS]: One report per sampled bucket (empty when the
            check is disabled via configuration).
        """
        findings = []

        if not s3_client.audit_config.get("s3_bucket_object_public_enabled", False):
            return findings

        for bucket in s3_client.buckets.values():
            sampling = bucket.object_sampling
            # Sampling is populated by the service layer only when the check is
            # enabled; skip any bucket that was not sampled.
            if sampling is None or not sampling.performed:
                continue

            report = Check_Report_AWS(metadata=self.metadata(), resource=bucket)

            if sampling.error_code is not None:
                report.status = "MANUAL"
                if sampling.error_code == "AccessDenied":
                    report.status_extended = (
                        f"Access Denied when spot-checking objects in bucket "
                        f"{bucket.name}."
                    )
                else:
                    report.status_extended = (
                        f"Could not spot-check objects in bucket {bucket.name}: "
                        f"{sampling.error_message}."
                    )
            elif sampling.is_empty:
                report.status = "PASS"
                report.status_extended = f"S3 Bucket {bucket.name} is empty."
            else:
                public_objects = [
                    obj.key
                    for obj in sampling.objects
                    if any(
                        grantee.type == "Group" and grantee.URI in PUBLIC_ACL_URIS
                        for grantee in obj.grantees
                    )
                ]
                sampled = len(sampling.objects)

                if public_objects:
                    report.status = "FAIL"
                    report.status_extended = (
                        f"S3 Bucket {bucket.name} has public objects detected in "
                        f"spot-check sample of {sampled} objects: "
                        f"{', '.join(public_objects)}."
                    )
                else:
                    report.status = "PASS"
                    report.status_extended = (
                        f"No public objects detected in spot-check sample of "
                        f"{sampled} objects in bucket {bucket.name}. For complete "
                        f"assurance, ensure ACLs are disabled via Object Ownership "
                        f"settings."
                    )

            findings.append(report)

        return findings