Back to Checks
aws

S3 bucket is not a known shadow resource owned by another account

s3_bucket_shadow_resource_vulnerability
Service: s3Severity: high
OfficialBuilt-inVerified
Categories:trust-boundaries
Resource Type:AwsS3Bucket

Amazon S3 buckets using predictable service naming (e.g., aws-glue-assets-<account>-<region>, sagemaker-<region>-<account>) are identified and their ownership checked.

Buckets tied to your account that match these patterns but are owned by another account-across regions-are surfaced as shadow-resource candidates.

Risk

Preclaimed buckets matching your account's patterns let outsiders intercept service artifacts, causing:

  • Loss of confidentiality (templates, data exfiltration)
  • Compromised integrity (script/config injection RCE, privilege escalation)
  • Reduced availability (creation failures or redirected writes)

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks s3_bucket_shadow_resource_vulnerability

Recommendation

Apply defense in depth:

  • Preprovision and own required service buckets in all current and planned regions
  • Enforce least privilege so services write only to approved bucket names/ARNs
  • Use non-guessable names where you control naming
  • Monitor for look-alike buckets and separate duties for bucket creation vs. use

Remediation

Terraform

# Terraform: pre-claim the predictable S3 bucket name to ensure your account owns it
resource "aws_s3_bucket" "<example_resource_name>" {
  bucket = "<FLAGGED_BUCKET_NAME>"  # Critical: create the exact bucket name reported by the check so it is owned by this account
}

NativeIaC

# CloudFormation: pre-claim the predictable S3 bucket name to ensure your account owns it
Resources:
  <example_resource_name>:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: <FLAGGED_BUCKET_NAME>  # Critical: create the exact bucket name reported by the check so it is owned by this account

Other

1In the Prowler finding, copy the exact bucket name flagged (e.g., aws-glue-assets-<ACCOUNT_ID>-<REGION>).
2Open the AWS S3 console and click Create bucket.
3Paste the exact bucket name and select the region that matches the name.
4Click Create bucket.
5Repeat for each flagged name/region so all predictable service buckets are owned by your account.

Check Code

import re

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


class s3_bucket_shadow_resource_vulnerability(Check):
    def execute(self):
        findings = []
        # Predictable bucket name patterns from the research article
        # These patterns are used by AWS services and can be claimed by attackers
        predictable_patterns = {
            "Glue": f"aws-glue-assets-{s3_client.provider.identity.account}-<region>",
            "SageMaker": f"sagemaker-<region>-{s3_client.provider.identity.account}",
            # "CloudFormation": "cf-templates-.*-<region>",
            "EMR": f"aws-emr-studio-{s3_client.provider.identity.account}-<region>",
            "CodeStar": f"aws-codestar-<region>-{s3_client.provider.identity.account}",
            # Add other patterns here as they are discovered
        }

        # Track buckets we've already reported to avoid duplicates
        reported_buckets = set()

        # First, check buckets in the current account
        for bucket in s3_client.buckets.values():
            # Only emit a finding when the bucket name actually matches one of
            # the predictable service patterns. A bucket whose name does not
            # match any pattern is, by definition, not a shadow resource, so a
            # PASS finding for it would be tautological and add no signal.
            for service, pattern_format in predictable_patterns.items():
                pattern = pattern_format.replace("<region>", bucket.region)

                if re.match(pattern, bucket.name):
                    report = Check_Report_AWS(self.metadata(), resource=bucket)
                    report.region = bucket.region
                    report.resource_id = bucket.name
                    report.resource_arn = bucket.arn
                    report.resource_tags = bucket.tags

                    if bucket.owner_id != s3_client.audited_canonical_id:
                        report.status = "FAIL"
                        report.status_extended = f"S3 bucket {bucket.name} for service {service} is a known shadow resource and it is owned by another account ({bucket.owner_id})."
                    else:
                        report.status = "PASS"
                        report.status_extended = f"S3 bucket {bucket.name} for service {service} is a known shadow resource but it is correctly owned by the audited account."

                    findings.append(report)
                    reported_buckets.add(bucket.name)
                    break

        # Now check for shadow resources in other accounts by testing predictable patterns
        # We'll test different regions to see if shadow resources exist
        regions_to_test = (
            s3_client.provider.identity.audited_regions
            or s3_client.regional_clients.keys()
        )

        for region in regions_to_test:
            for service, pattern_format in predictable_patterns.items():
                # Generate bucket name for this region
                bucket_name = pattern_format.replace("<region>", region)

                # Skip if we've already reported this bucket
                if bucket_name in reported_buckets:
                    continue

                logger.info(
                    f"Checking if shadow resource bucket {bucket_name} exists in other accounts"
                )
                # Check if this bucket exists in another account
                if s3_client._head_bucket(bucket_name):
                    report = Check_Report_AWS(self.metadata(), resource={})
                    report.region = region
                    report.resource_id = bucket_name
                    report.resource_arn = (
                        f"arn:{s3_client.audited_partition}:s3:::{bucket_name}"
                    )
                    report.resource_tags = []
                    report.status = "FAIL"
                    report.status_extended = f"S3 bucket {bucket_name} for service {service} is a known shadow resource that exists and is owned by another account."

                    findings.append(report)
                    reported_buckets.add(bucket_name)

        return findings