The Docs Say It's OK: When Documentation Replaces a Fix in Vertex AI
What if the fix for a privilege-escalation bug were a sentence in the docs? Not a patched code path, not the missing check but a note on a reference page. That's not hypothetical. It's what Google shipped for two new privilege escalation paths in Vertex AI.
That's the whole story, and it's also the problem. Somewhere along the line, "we wrote it down" became an acceptable answer to "you have a privilege-escalation bug."
It isn't. Documenting a vulnerability doesn't mitigate it β it just moves the risk from Google's backlog to your audit checklist. The warning boxes that now sit next to IAM roles read less like release notes and more like the black-box warning on a bottle of prescription pills: here are the serious side effects; good luck in figuring out what it means.
Both bugs discussed in this blog live in Vertex AI, and both are the same three-part failure.
- One: a caller runs code inside a Google-managed runtime β for Custom Jobs, that's the whole point of the feature; for Reasoning Engines, it's an unsafe deserialization flaw.
- Two: that runtime runs the code as a Google-owned service agent that's automatically granted a highly-privileged role, and leaves its token where the code can simply read it.
- Three: rather than close the path, Google has chosen to document it and haphazardly at that, in notes that still don't spell out the full reach.
This isn't a confused deputy tricked into acting on your behalf; it's you holding the deputy's credentials outright and inheriting every permission it has.
Here's the TL;DR facts on both vulnerabilities, before we get into how each one works:
Vertex AI Custom Jobs: Editor-Level Access, Unchecked
Anyone who can start a custom training job can make their code run as a highly privileged Google-managed identity, lift that identity's token, and, with it, mint access tokens for every service account in your project.
The permission is aiplatform.customJobs.create. It's what you hand anyone who needs to run a Vertex AI custom training job, such as a data scientist, an ML engineer, or a pipeline. On its face, it carries the expectation of code execution. In practice, it carries a great deal more than what usually gets threat-modeled.
Here's the mechanism. The projects.locations.customJobs.create method takes a CustomJob you supply, and in that spec jobSpec.serviceAccount is optional. Leave it out, and Google runs your training container as the project's Custom Code Service Agent β service-PROJECT_NUMBER@gcp-sa-aiplatform-cc.iam.gserviceaccount.com β a Google-managed identity Google calls a Per-Product Per-Project Service Account (P4SA). That agent holds the roles/aiplatform.customCodeServiceAgent automatically, an extensive permission set that includes, most dangerously, a series of IAM permissions, including iam.serviceAccounts.getAccessToken, signBlob, and signJwt, allowing for service account impersonation plus project Storage.
Two guardrails might have applied here. Neither does.
The actAs gate is the control you'd expect to catch this, but iam.serviceAccounts.actAs governs only customer-managed service accounts, guarding the attachment of a given service account to a workload. In this case, the caller never attaches one, so there's nothing for .actAs to gate. And a caller-authorization check has no place to run here either. That control verifies you're allowed to reach a resource a deputy is fetching on your behalf, and here no deputy is fetching anything.
So your code simply reads the Custom Code Service Agent's token from the metadata server and automatically acquires all assigned permissions. Every .getAccessToken call after that is the P4SA acting on its own, with its own credentials.
This isn't a check that failed to fire. It's a privileged identity handed to whoever can run code in the box.
The offending permission, aiplatform.customJobs.create, is bundled into a long list of roles. Besides the always-obvious Owner and Editor:
Roles you grant to people:
- Agent Platform User (roles/aiplatform.user)
- Data Scientist (roles/iam.dataScientist)
- ML Engineer (roles/iam.mlEngineer)
- Agent Platform Administrator (roles/aiplatform.admin)
- Aiplatform Editor (roles/aiplatform.editor)
Service-agent roles:
- roles/aiplatform.customCodeServiceAgent
- roles/aiplatform.serviceAgent
- roles/notebooks.serviceAgent
- roles/visualinspection.serviceAgent
I reported this to the Google Cloud VRP on 2026-05-25 as b/516484193. On 2026-07-04, I was told it was a duplicate β "the vulnerability where omitting the service account field during job creation bypasses actAs validation and implicitly executes workloads as the highly privileged Custom Code Service Agent." A duplicate, that is, of the same behavior Google chose to document rather than fix. Which is when I noticed the new note on the Custom Code Service Agent role page.

Deciding to document rather than remediate is a sharp departure from the security norms customers expect.
Vertex AI Reasoning Engines: RCE via Unsafe Deserialization
Anyone you let create a Reasoning Engine can feed it a malicious Pickle from Cloud Storage, execute code as the Reasoning Engine Service Agent, lift its token, and inherit that agent's automatic, project-wide reach.
The permission is aiplatform.reasoningEngines.create β the second permission the docs partly come clean about, and one you hand to the same crowd: data scientists, ML engineers, anyone building on the platform.
Here's the mechanism. The create method accepts a PackageSpec that deserializes a Pickle object from Cloud Storage. A maliciously crafted pickle object gets you remote code execution inside the Reasoning Engine sandbox.
From there, you read the Reasoning Engine Service Agent's token - the agent assigned roles/aiplatform.reasoningEngineServiceAgent and inherit every permission automatically assigned to it. That's read/write on your project's Cloud Storage. Less obviously, it's also developerconnect.gitRepositoryLinks.fetchReadToken and developerconnect.gitRepositoryLinks.get β read tokens for any git repository you've connected through Developer Connect.
The blast radius isn't just your storage objects and buckets; it also includes your connected source repositories.
Same shape as before: you don't deputize the service agent to do something for you; you become it. Its token is right there in the runtime, so its automatic, project-scoped grants become yours. What's notable here is that the docs said the quiet part out loud ahead of time: the Reasoning Engine Service Agent role page already tells customers that aiplatform.reasoningEngines.create implies read access to the project's GCS objects but says nothing about other notable permissions, like the Developer Connect git token access, which sits in the very same role.
As with Custom Jobs, this permission rides a long list of roles. Besides the always-obvious Owner and Editor:
Roles you grant to people:
- Agent Platform User (roles/aiplatform.user)
- Data Scientist (roles/iam.dataScientist)
- ML Engineer (roles/iam.mlEngineer)
- Agent Platform Administrator (roles/aiplatform.admin)
- Aiplatform Editor (roles/aiplatform.editor)
- Agent Platform Express User (roles/aiplatform.expressUser)
- Agent Platform Express Admin (roles/aiplatform.expressAdmin)
Service-agent roles:
- roles/aiplatform.customCodeServiceAgent
- roles/aiplatform.serviceAgent
- roles/discoveryengine.serviceAgent
- roles/visualinspection.serviceAgent
I reported this to the Google Cloud VRP on 2026-05-27 as b/517210604 β remote code execution leading to service-account takeover. On 2026-06-08 it was closed "Won't Fix / Intended Behavior," with Google saying they'd "decided not to track it as a security bug." Its at this time I noticed the escalation path had been documented all along

Here's the trouble with documenting a privilege-escalation path, patched or documented: each path gets handled one at a time, but only documenting turns your reference pages into a row of black-box warnings while the design that creates them stays put.
Follow the Chain
Two bugs, one shape. In each, an everyday role gives you a permission β the permission lets your code run inside a Google-managed runtime βthat runtime executes it as a highly-privileged, Google-owned service account β and the account's token is right there for the taking. Once you hold it, its reach is yours. There's no deputy being tricked and no check being skipped; Google runs your code as a privileged identity and lets you keep its credentials. It's worth tracing each chain link by link because the distance between "I granted someone Data Scientist" and "someone is reading every bucket in the project" is much shorter than it may feel.
Reasoning Engines: caller role β RCE β service-agent token β profit

- Start β any principal holding a role that contains aiplatform.reasoningEngines.create.
- Trigger β they call reasoningEngines.create with a PackageSpec pointing at a poisoned Pickle in Cloud Storage.
- RCE β the platform deserializes the Pickle (unsafe by design) and their code runs in the Reasoning Engine sandbox, as the Reasoning Engine Service Agent.
- Token β they read the agent's token from the runtime; they now hold its credentials.
- Reach β that token reads and writes all project Cloud Storage and fetches read tokens for any git repo linked through Developer Connect.
Custom Jobs: caller role β P4SA token β potential project editor

- Start β any principal holding a role that contains aiplatform.customJobs.create.
- Trigger β they call customJobs.create and simply omit the optional jobSpec.serviceAccount field, so the job runs as the P4SA (no customer SA named, so actAs never applies).
- Token β the container runs as the Custom Code Service Agent; they read its token from the metadata server and hold its credentials.
- Reach β that token carries getAccessToken, signBlob, and signJwt, so they mint tokens for any service account in the project; with project Storage on top, that's an effective project editor.
Same move in both chains: run code in a managed box, read the privileged identity's token, keep everything it can reach. The caller role at the top of the chain simply inherits everything downstream of it.
A Very Large Exception
These two Vertex AI service agents were Google's to use and manage. Now they're yours to run directly as well, with every permission they automatically carry. It's a first: a highly privileged, provider-managed identity that the customer can simply pick up and run off with. And the exception was carved with a footnote. What does that mean in practice for Google customers? Here's the documentation Google didn't write:
- Treat aiplatform.customJobs.create β and every role that contains it, including AI Platform User, Data Scientist, and ML Engineer as potentially project-editor-equivalent. Grant and audit it that way.
- Treat aiplatform.reasoningEngines.create β and every role that contains it as conferring project-wide Cloud Storage read/write and read access to the git-repository tokens you've connected through Developer Connect.
- Use the project as your blast-radius boundary. Isolate Vertex AI workloads so that the effective-editor and Storage-reader reach stay contained within a single project.
- Ask your Google TAM whether these managed runtimes can run under a scoped-down or customer-supplied identity instead of a project-wide P4SA β and whether there is a supported way to shrink the service agents' grants below the project level.
Customers rely on providers not to build privilege-escalation pathways into their own IAM schemes and, if they do surface, to close them rather than annotate them.
Google wrote the side effects on the label and called it a day. But a label doesn't lower the risk; it just moves it onto whoever didn't read the fine print. Documenting a vulnerability is still just a vulnerability.
Reporting Timeline
Custom Jobs β customJobs.create, b/516484193
- 2026-05-25 β [kat]: Reported to the Google Cloud VRP: omitting the optional jobSpec.serviceAccount field runs the training container as the Custom Code Service Agent (a P4SA), whose token can mint access tokens for any service account in the project. Auto-acknowledged.
- 2026-07-04 β [VRP]: Closed as a duplicate β of "the vulnerability where omitting the service account field during job creation bypasses actAs validation and implicitly executes workloads as the highly privileged Custom Code Service Agent." A duplicate, in other words, of the very behavior Google had already decided to document rather than fix.
- 2026-08-17 β [kat]: I updated the ticket to say I'd be writing this up as a public blog post outlining the themes to expect.
Reasoning Engines β reasoningEngines.create, b/517210604
- 2026-05-27 β [kat]: Reported to the Google Cloud VRP: managed-runtime code execution as the Vertex AI service agent, leading to service-account impersonation and project takeover. Auto-acknowledged.
- 2026-06-08 β [VRP]: Report closed. Not tracked as a security bug; per Google, it "does not meet the threshold" for this type of escalation.
- 2026-06-08 β [kat]: I notice the documentation note on the service-agent roles page and point out that the automatic, project-level grants are the problem.
- 2026-06-10 β [VRP]: Since the behavior is documented, it doesn't demonstrate sufficient security impact to be in scope; feedback belongs in the Cloud forums.
- 2026-08-17 β [kat]: I updated the ticket to say I'd be writing this up as a public blog post outlining the themes to expect.
For the curious, I tried to pin down when these documentation changes landed on the Vertex Custom Code and Reasoning Engine service-agent role pages:
- The Custom Code "highly privileged / editor-level access" note: the first capture of the page that shows the warning box is from after my report, on June 4th.
- The Reasoning Engine service-agent note β that reasoningEngines.create implies read access to the project's GCS objects β was already live before my report; it was first captured around 2026-04-20, roughly five weeks prior.