Back to Checks
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 CloudRun
prowler aws --checks s3_bucket_shadow_resource_vulnerabilityRecommendation
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 accountOther
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