Varonis Threat Labs uncovered a cross-tenant Google Cloud Dataflow vulnerability that potentially allowed attackers to exploit predictable bucket names and achieve remote code execution (RCE) on another organization's Dataflow workers, without any access to the victim's project. Under the right conditions, the attack started from just the target's GCP project ID.
After our Dataflow Rider research, we examined Apache Beam, the open-source SDK behind Dataflow, and found that under certain conditions, attackers could run code in projects they could not access directly. By pairing a predictable bucket-naming scheme with a flag already used by many Dataflow jobs, they could exploit the same service account compromised in Dataflow Rider, without requiring any initial access to the victim's environment.
The victim had to be running a pipeline built with Beam's Python SDK, and it had to be the project's first Dataflow job in that region to use the default staging bucket. Google has since mitigated the issue, with fixes rolled out globally that protect even older or pinned SDK versions.
The predictable staging bucket
Inside gcsio.py we learned how Beam decides where a pipeline's staging bucket lives when the pipeline doesn't specify one.
If a pipeline doesn't configure an explicit --staging_location, Beam computes a default bucket name deterministically from the project ID and region:
GCP project IDs are not secrets. They show up in URLs, IAM bindings, error messages, support tickets, and are often guessable from a company's name. MD5 provides no protection here either, as it's purely used to shorten the string into a valid bucket-name component, and it's trivial to precompute for any project ID an attacker wants to target.
That predictable name is then used to look the bucket up and create it if it doesn't exist:
GCS bucket names are a global namespace. Only one project in the world can own dataflow-staging-<region>-<hash> at a time. Whoever creates it first, wins. This is textbook bucket squatting: an attacker computes the victim's future staging bucket name ahead of time, creates it in their own project, and grants AllUsers access to it.
When the victim later runs its first Dataflow job in that region without a dedicated staging bucket, get_or_create_default_gcs_bucket() finds the attacker's bucket already sitting at that name and simply reuses it. Unlike Beam's Java SDK, which already verified that the bucket belonged to the project, the Python SDK never checked who owned it.
The victim's own pipeline stages its code into infrastructure the attacker controls, and the attacker doesn't need a single IAM permission on the victim's project to pull it off. The same first-come rule also limits the attack: once a project has created its default bucket in a region, it owns that name for good. The opening exists only for projects that haven't yet run a default-bucket Dataflow job in that region.
A squatted bucket by itself is only a foothold. The second piece, the one that turns it into code execution, is what a lot of pipelines upload into that bucket without a second thought: --save_main_session.
From a guessable bucket to code execution
Every Dataflow pipeline is just a Python script. When that script relies on global state, such as imported modules, helper functions, or constants defined at the top level, the workers that execute the pipeline need a copy of that state too.
At the time of our research, Dataflow trusted staged artifacts pulled from storage without validating them, as we saw in Dataflow Rider (Google has since added integrity verification, covered below). However, this artifact isn't a YAML template or a UDF whose logic the pipeline merely interprets. It's a raw pickle stream, and unpickling a pickle stream in Python doesn't just restore data, it can execute code. Any object that defines __reduce__ gets to decide what runs the moment it's deserialized:
Put the predictable bucket name and the pickled session together, and a bucket an attacker squats before the victim ever runs a job becomes a delivery mechanism for arbitrary code execution on that victim's Dataflow workers.
Chaining it together: squat, watch, and swap
Put bucket squatting and code execution together, and you get an attack path that looks like this:
- Prepare a malicious staging bucket in the attacker's organization. For a target project ID and one or more likely regions, precompute dataflow-staging-{region}-{md5(project_id)} and create each bucket in an attacker-owned project, with public access prevention disabled and AllUsers granted read/write.
- Generate a malicious pickle payload in the attacker's organization. Build a Python object whose __reduce__ method reaches out to the instance metadata server, retrieves the worker service account's identity and access token, and exfiltrates both to an attacker-controlled endpoint.
- Wait for the victim to run a --save_main_session job without an explicit staging bucket configured, in a region where the project hasn't yet created its default bucket. Dataflow creates the job's working folder inside the attacker's squatted bucket and uploads the legitimate pickled_main_session into it.
- Detect and swap. The attacker polls the bucket for new job-prefixed folders, and the moment pickled_main_session appears, deletes it and re-uploads the malicious pickle from step 2, racing the workers that are about to start.
- Wait for execution. Every worker that boots for that job, including new workers spun up by autoscaling, downloads and unpickles the swapped file, running the attacker's code with the permissions of the Dataflow worker's service account. By default, that service account can be broadly privileged, such as the Compute Engine default service account.
Attack flow video
The video below walks through the full attack flow, from squatting the predictable staging bucket to swapping the pickled session and executing code on Dataflow workers.
Impact
The impact here depends on the privileges of the job's runtime service account, but can include:
- Credential theft: exfiltration of the Dataflow worker service account's OAuth token via the instance metadata endpoint.
- Code execution: arbitrary code execution on every Dataflow worker for the job, including workers spun up later by autoscaling.
- Privilege escalation / lateral movement: using the stolen token to pivot to other GCP resources the service account can reach.
- Data manipulation or exfiltration: since the payload runs inside the same worker process handling the pipeline's data, and because the attacker-controlled staging bucket may expose staged artifacts and job outputs written there for exfiltration as well.
The fix
We reported our bucket-squatting discovery to the Apache Beam project, and a fix was implemented in June 2026. We debuted our research and insights at DEF CON 2026 in the Cloud Village. Google was fast, responsive, and professional throughout the disclosure process, and the Google Cloud VRP team thanked us for our report and partnership and for helping keep Google safe.
Google addressed the issue in three layers, all of which are now rolled out globally.
Layer 1: An ownership check in the Beam Python SDK
Google contributed a client-side check to the Apache Beam Python SDK (apache/beam PR #39008, released in Beam 2.75.0). It adds an explicit ownership check before Beam trusts a bucket it didn't just create itself.
get_or_create_default_gcs_bucket() now calls _validate_bucket_project() on any pre-existing bucket it finds at the predictable default name, resolving the caller's project ID to a project number via the Cloud Resource Manager API and comparing it to the bucket's actual owning project number.
If an attacker has already squatted the predictable bucket name in their own project, the ownership check causes the project numbers to differ, so Beam raises a ValueError instead of silently staging the job’s code into attacker-controlled storage.
This closes the bucket-squatting path on the client side for jobs running Beam 2.75.0 or later.
Layer 2: Server-side enforcement in the Dataflow service
Google also deployed the same kind of check directly in the Dataflow service backend. Whenever a job is submitted using a default staging bucket, the Dataflow control plane independently verifies that the bucket is owned by the submitting project and rejects the job with FAILED_PRECONDITION if it belongs to another project. Because this is enforced server-side, customers running older or pinned Beam SDK versions are protected against default-bucket squatting too, without needing to upgrade.
Layer 3: End-to-end integrity checks for staged artifacts
Finally, Google rolled out SHA-256 integrity verification for staged files across Dataflow and Apache Beam. During staging, the SDK computes SHA-256 hashes of staged artifacts, including pickled_main_session, and records them. Dataflow workers verify the digest of every downloaded artifact on startup and abort if any file in GCS was modified or swapped after staging. That shuts down the "detect and swap" race from step 4 of the attack.
Two important things to keep in mind:
- Explicit buckets are your call. The ownership checks apply to the auto-selected default staging bucket. Buckets you configure explicitly with --staging_location or --temp_location are intentionally allowed to live in a shared or centralized project, so make sure that project and its bucket permissions are locked down.
- Pickle is still pickle. --save_main_session still serializes your session with pickle, which can execute code by design. Integrity verification now stops staged files from being swapped after upload, but the staging bucket is still part of your code supply chain. Treat write access to it like write access to your source code.
Closing thoughts
Cloud services are full of invisible assumptions. A bucket with the right name is assumed to belong to the right project, just like a staged artifact is assumed to be the one the job intended to run. By themselves, those assumptions can look harmless. Chained together, they can turn a predictable name into a cross-tenant execution path.
That is the larger lesson from this research. Trust is often inherited through defaults, naming conventions, and automation in cloud environments, not just through explicit permissions. Attackers look for those seams to scale and learn predictable patterns. Don’t let them linger in your infrastructure to complete the chain.



/)
/)




