- GCS bucket names are globally unique, so a predictable staging bucket that does not exist yet can be claimed by anyone, in any project.
- Vertex AI training and pipeline jobs execute code and load artifacts from those buckets, which turns bucket control into remote code execution.
- Set the staging bucket explicitly to one you pre-created, and scope its IAM to the job's service account only.
- Assessors want an inventory of every bucket your ML workloads read from, and proof that you own each one.
What bucket squatting actually is
Google Cloud Storage runs a single global namespace for bucket names. There is exactly one bucket called acme-vertex-staging across all of Google Cloud, and the first project to create that name owns it. That design is what makes squatting possible. If a tool, a tutorial, or your own code refers to a bucket that has not been created yet, and the name is predictable, an attacker can create it first in a project they control.
It is the same failure pattern as dependency confusion and subdomain takeover. Something you depend on is addressed by a name, the name resolves to a resource nobody has claimed, and an outsider claims it. Once they hold the name, every read your workload performs against that bucket returns content they chose.
Predictability is the whole game. Names built from a project ID, a company prefix, a region, or a fixed suffix copied from a quickstart are easy to enumerate. An attacker does not need access to your project to guess a name like project-id-vertex-pipelines or company-model-artifacts and register the ones that are still free.
Why Vertex AI turns a squatted bucket into code execution
Machine learning pipelines run code by design, so control over an input bucket rarely stays a data problem. It becomes execution.
A custom training job stages a training package or reads a container spec from a bucket, then runs it. A pipeline reads component definitions and step inputs from its pipeline root, which is a bucket. Jobs install dependencies from a requirements file or a wheel that may live in storage. Model artifacts are often serialised objects, and loading a poisoned pickle runs attacker code the moment it is deserialised. In every one of these paths, whoever controls the bucket controls what executes.
The blast radius comes from the service account. Training and serving jobs run as a service account, and in a lot of deployments that identity carries far more than it needs: write access to other buckets, permission to deploy endpoints, sometimes project-level roles. Code execution inside the job inherits all of it, so a single squatted bucket can become a foothold into the wider Google Cloud project.
Where the predictable names come from
Most of the exposure is created before anyone writes a line of application code.
The SDK's convenience defaults are the first source. When you call the init helper without setting a staging bucket, or use helpers that create a default staging area, you can end up depending on a name derived from your project rather than one you deliberately provisioned and locked down. If that name has not been created and claimed, it is squattable until it is.
Copied tutorial code is the second. Quickstarts use fixed bucket names to keep the example short. Those names get pasted into notebooks, Cloud Functions, and CI jobs, then shipped without anyone treating the bucket as a security boundary.
Infrastructure drift is the third. A bucket referenced in Terraform or a deploy script but never actually created, a bucket deleted during cleanup while code still points at it, or an environment promoted from dev to prod with a name that was never registered in the new region. Each one leaves a live reference to a name that anyone can take.
What goes wrong in real deployments
The vulnerability is rarely a single bug. It is a chain of reasonable-looking defaults.
Teams accept the auto-created bucket because it works on the first run. Nobody pre-creates the staging bucket, so the reference sits unclaimed between the time code is written and the time it first runs, which is exactly the window an attacker needs. The training service account is left with broad roles because tightening it breaks a quickstart. Data access logging on the bucket is off, so a read from a bucket you do not own leaves no trace. And because GCS names are global, claiming a name in a region you have not used yet is trivial, so multi-region and multi-project setups widen the target.
None of these are exotic. They are the path of least resistance, which is why the honest version of this problem is a governance gap, not a zero-day.
What assessors ask for
When this comes up in a review, the register is evidence, not intentions. Expect to be asked for the following.
- A current inventory of every Cloud Storage bucket your ML workloads read from or write to, including staging buckets, pipeline roots, and artifact locations.
- Proof that each of those buckets exists in a project you own and was created deliberately, not on demand.
- IAM on those buckets showing uniform bucket-level access and permissions scoped to the specific service accounts that need them.
- The roles held by training and serving service accounts, with a least-privilege justification for anything beyond read and write to the named buckets.
- Cloud Audit Logs, including Data Access logs, on the buckets, plus alerting on unexpected reads and on bucket creation across the organisation.
- Organisation policy constraints that limit who can create buckets and where.
If you cannot produce the bucket inventory quickly, that is usually the finding. You cannot defend names you have never enumerated.
How to close it
Own and pin every bucket
Pre-create the buckets your workloads use, in a project you control, before any code references them. Pass the staging bucket explicitly through the SDK's init call, and set the pipeline root to a bucket you provisioned. Never depend on a default that is created on first use. A name you already own cannot be squatted.
Scope the service account down
Give training and serving identities the narrowest roles that let the job run: read and write to the named buckets, and nothing project-wide by default. This is the single control that limits what a poisoned artifact can do if one ever lands. It also survives version changes in the SDK, which feature-level fixes do not.
Control what the pipeline loads
Pin dependencies to versions and hashes, install from a private Artifact Registry rather than an open reference, and avoid loading serialised models from any location you do not own. Where you can, prefer artifact formats that do not execute code on load over raw pickle files.
Detect and validate
Continuous posture management flags the conditions that make squatting possible: buckets with weak IAM, service accounts with excess roles, and references to buckets that do not exist. Microsoft's Defender for Cloud posture management and the cloud modules in comparable vulnerability platforms surface these across accounts. Then prove it offensively: a scoped cloud penetration test should try to enumerate and claim your predictable bucket names before someone else does, the same discipline covered in our write-up on cloud VAPT and S3-style bucket risks.
The UAE governance angle
For UAE and GCC buyers, this lands on two questions that come up in most cloud reviews: where the data sits, and who can touch it. Bucket location matters for data residency, so ML staging and artifact buckets holding regulated data should be created in the region your policy allows, and that region should be claimed by you rather than left open. Assessors in banking and government engagements expect least-privilege service accounts and audit logging on these stores as standard evidence, and they treat an unmanaged, auto-created bucket the same way they treat any other ungoverned data store. Treat your Vertex AI storage as part of your cloud security posture, not as scratch space.
A short checklist before you ship a Vertex AI workload
- Every bucket the workload touches is pre-created in a project you own.
- The staging bucket and pipeline root are set explicitly, never left to a default.
- The job's service account is scoped to those buckets and nothing more.
- Dependencies are pinned and pulled from a private registry.
- Data Access logging is on, with alerts on unexpected reads and new bucket creation.
- The SDK is on a current, patched version.
If any line is unchecked, assume the name is squattable and fix that before the workload runs.