Google ships a control that makes this entire bug class impossible: Public Access Prevention. Turn it on at the organization level and no one (no rushed engineer, no copy-pasted Stack Overflow command, no Terraform module someone vendored in 2021) can ever grant allUsers access to a bucket again. The binding is simply rejected. It is a hard, org-wide control, and it is the single most effective thing you can do about GCS exposure.

Most organizations never set it. So instead of one switch, they are betting that every operator on every project, forever, never runs one bad add-iam-policy-binding --member=allUsers. That is the real security posture of most GCS estates, and it is why open GCS buckets stay common: a single binding to the allUsers principal lets any anonymous HTTP client list and read the bucket, exactly like an open S3 bucket.

How GCS buckets become publicly accessible

Google Cloud Storage access is governed by Cloud IAM. Every bucket has an IAM policy that specifies who can do what. Two special principal identifiers can make a bucket public:

  • allUsers: any caller on the internet, authenticated or not. No Google account is required. This is the principal that creates fully anonymous public access.
  • allAuthenticatedUsers: any caller authenticated with any Google Account or service account.

People routinely treat allAuthenticatedUsers as the "safe" version of public, because it needs a login. Read it again: any Google account. A free personal Gmail address someone created in thirty seconds is an authenticated Google account. So is every other Gmail address on earth. This principal does not mean "people in my org" or "people I've invited"; it means roughly four billion strangers who happen to have a Google login. As an access boundary it is barely distinguishable from allUsers, and treating it as a meaningful restriction is one of the more dangerous assumptions in GCS.

The most common role granted to these principals is Storage Object Viewer (roles/storage.objectViewer), which allows reading and listing all objects in the bucket. Granting allUsers this role is equivalent to making the bucket fully public. Other roles that also expose data include Storage Legacy Bucket Reader (roles/storage.legacyBucketReader), which grants list access.

Uniform Bucket-Level Access vs legacy ACLs

GCS has two access-control systems, and you need to understand both to audit a bucket fully.

Uniform Bucket-Level Access (UBLA) uses only Cloud IAM policies. When UBLA is enabled on a bucket, object-level ACLs are disabled and all access decisions are made by the bucket's IAM policy. This is the simpler, easier-to-audit model.

Legacy access control uses both Cloud IAM policies and per-object ACLs. When UBLA is disabled (the legacy mode), object-level ACLs can grant public access independently of the bucket's IAM policy. A bucket IAM policy might look locked down while individual objects still carry allUsers ACLs from an earlier configuration. Buckets created before UBLA became generally available in 2019, and buckets created without explicitly enabling UBLA, may carry this legacy mode with inconsistent object-level ACLs.

Public Access Prevention, in detail

Public Access Prevention (PAP) is the control from the opening. It blocks allUsers and allAuthenticatedUsers from ever being granted IAM roles on a bucket or its objects, and it blocks public ACLs too. You can set it on an individual bucket, or (far better) enforce it across a whole project or organization through the constraints/storage.publicAccessPrevention Organization Policy constraint. When PAP is enforced, any IAM binding that would open a bucket to those two principals is rejected at the API, not merely flagged after the fact.

It is not on by default. A bucket created today is perfectly happy to accept an allUsers binding unless something above it says no. Set the org policy once and the bad command stops working everywhere, for everyone, including the parts of your estate you have forgotten you own.

How to detect anonymous access

Unauthenticated listing. GCS exposes two public listing endpoints. If a bucket grants allUsers list access, the following requests return an object manifest without any Authorization header:

JSON API (returns JSON):

GET https://storage.googleapis.com/storage/v1/b/<bucket-name>/o

XML API (returns XML):

GET https://storage.googleapis.com/<bucket-name>

A 200 response with XML object data confirms public list access. The signal that a bucket is not publicly listable is a 403: AccessDenied from the XML API, or a forbidden reason from the JSON API. Do not read accessNotConfigured as "the bucket is private": it means the Cloud Storage JSON API is not enabled in your calling project, which is a problem with your request, not with the target bucket's exposure.

Google Cloud CLI IAM policy check. If you own the bucket or have sufficient IAM permissions:

gcloud storage buckets get-iam-policy gs://<bucket-name>

Review the output for any binding where the members list contains allUsers or allAuthenticatedUsers. Any such binding represents a public access grant.

Check UBLA status.

gcloud storage buckets describe gs://<bucket-name> \
  --format="value(iamConfiguration.uniformBucketLevelAccess.enabled)"

If the result is False, the bucket uses legacy ACLs and you should also audit object-level ACLs for allUsers grants.

Check a GCS bucket here. Paste the storage.googleapis.com URL into the open-bucket viewer to probe anonymously and confirm what an unauthenticated caller can access.

What's at risk

Public listing via the XML or JSON API returns every object's name, size, content type, and last-modified timestamp. This is enough to identify what's in the bucket and begin targeted downloads. Filename patterns alone can reveal sensitive content: user-export-2025-08-01.csv, backup-db-prod.tar.gz, config-secrets.json.

Public object read allows downloading any object whose URL is known or guessable. When combined with listing, this means full enumeration and download of every file in the bucket with a simple loop.

What makes GCS exposure sting is what GCS is used for. It is the default landing zone for data pipelines, so BigQuery table exports (frequently full customer or financial tables, dumped flat as CSV or Avro) end up sitting in a bucket. It is where Cloud Build drops artifacts, which means compiled binaries and the intermediate build state that comes with them. Teams pipe Cloud Logging and Audit log exports into it, handing an anonymous reader a map of the internal infrastructure. And because a bucket is cheap and convenient, static site assets routinely share one with the application config that was never meant to leave the building. The same anonymous GET reaches all of it.

Remediation

Remove allUsers and allAuthenticatedUsers IAM bindings. For each offending binding:

gcloud storage buckets remove-iam-policy-binding gs://<bucket-name> \
  --member=allUsers \
  --role=roles/storage.objectViewer

Repeat for allAuthenticatedUsers and for any other role that grants list or read access. Run gcloud storage buckets get-iam-policy again afterward to confirm the binding is gone.

Enable Uniform Bucket-Level Access. Switching a bucket to UBLA disables object-level ACLs and centralizes all access control in the IAM policy, making the bucket's exposure surface easier to audit:

gcloud storage buckets update gs://<bucket-name> \
  --uniform-bucket-level-access

Note: enabling UBLA is reversible for 90 days after it is set; after 90 days, it cannot be disabled.

Enable Public Access Prevention. To prevent any future grant of allUsers or allAuthenticatedUsers on the bucket:

gcloud storage buckets update gs://<bucket-name> \
  --public-access-prevention

To enforce PAP across an entire project or organization, set the Organization Policy constraint constraints/storage.publicAccessPrevention to Enforced at the desired resource hierarchy level via the Google Cloud console or the gcloud org-policies command.

Use signed URLs for time-limited sharing. Rather than granting permanent public access for distribution use cases, generate signed URLs that expire after a set duration:

gcloud storage sign-url gs://<bucket-name>/<object-name> \
  --duration=1h \
  --private-key-file=<service-account-key.json>

Signed URLs grant read access to one object for one time period and cannot be used to enumerate other objects in the bucket.

Rotate credentials found in previously-public buckets. Any secrets, API keys, or credentials that may have been stored in the bucket's objects should be considered compromised for the duration the bucket was public. Rotate all such credentials immediately, regardless of what access logs show.


For a full index of publicly accessible Google Cloud Storage buckets found by this service, see the GCS exposure index. To test a specific GCS bucket URL right now, use the open-bucket viewer.