Vertex AI SDKのModel.uploadが予測可能なGCSバケット経由でモデル差し替えRCEにつながった
目次
TL;DR
影響 Vertex AI SDK for Python(google-cloud-aiplatform)の Model.upload() で、staging_bucket を明示せずにローカルモデルをアップロードする環境。Unit 42が確認した脆弱版は1.139.0と1.140.0
成立条件 対象プロジェクト・リージョンのデフォルトステージングバケットが未作成で、攻撃者が同名のCloud Storageバケットを先に作成済み。攻撃者側に被害者プロジェクトの権限は不要
対応 google-cloud-aiplatform を1.148.0以降へ更新し、モデルアップロード時は自分の管理するCloud Storageを staging_bucket に指定。ノートブック、CI、学習パイプライン、バッチ実行環境のSDK版まで確認
Palo Alto Networks Unit 42が、Vertex AI SDK for Pythonのモデルアップロード処理でバケットスクワッティング(予測できるCloud Storageバケット名を攻撃者が先に取る手口)からRCE(リモートコード実行)につながる欠陥を公開した。
対象は Model.upload() のデフォルトステージングバケット処理で、staging_bucket を指定しない場合に、SDKがプロジェクトIDとリージョンから PROJECT-vertex-staging-REGION 形式の名前を作っていた。
Googleは2026年3月31日の1.144.0で自動作成するステージングバケット名へUUID由来のsaltを追加し、2026年4月15日リリースの1.148.0でバケット所有者確認を追加した。 Unit 42の公開時点で、この問題に対応するCVEはGoogleのセキュリティ情報にもUnit 42の記事にも載っていない。
exists() だけ見て所有者を見ていなかった
問題は、SDKが「その名前のバケットが存在するか」を見て、存在した場合に「呼び出し元プロジェクトが所有するバケットか」まで確認していなかった点だ。
Cloud Storageのバケット名は全Google Cloudで一意なので、攻撃者が同じ名前のバケットを別プロジェクトで先に作ると、被害者側SDKの存在確認は通る。
Unit 42の説明では、攻撃者は被害者プロジェクトIDとリージョンを知っていればよい。
プロジェクトIDはドキュメント、サンプルコード、公開ログ、エラー出力、GitHub上の設定から漏れることがある。
被害者プロジェクトにログインする権限や、OAuthトークンの窃取は入口ではない。
成立条件は狭い。
対象リージョンでデフォルトステージングバケットがまだ作られておらず、かつ aiplatform.init() や Model.upload() で staging_bucket を明示していない場合に成立する。
新しいプロジェクトや、初めて使うリージョンでVertex AIへモデルを登録する流れが該当しやすい。
モデルはアップロード後2.5秒の間に差し替えられる
攻撃者は先取りしたバケットへ allAuthenticatedUsers 向けの読み書き権限を付け、被害者SDKからのアップロードとVertex AI側サービスエージェントからの読み取りを通す。
そのうえでCloud FunctionをCloud Storageのobject finalizeイベントへ接続し、被害者がアップロードした model.joblib を検知した瞬間に、悪意のあるjoblibファイルへ差し替える。
Unit 42のPoC(概念実証コード)では、被害者SDKのアップロードからVertex AIのサービスエージェントが読むまでの猶予は約2.5秒だった。
Cloud Functionは約804ミリ秒で反応し、約1.4秒でモデルを差し替え、約2.46秒後にサービスエージェントが差し替え後のファイルを読んだ。
flowchart TD
A["攻撃者が予測名の<br/>GCSバケットを作成"] --> B["被害者がstaging_bucket未指定で<br/>Model.upload()"]
B --> C["SDKが攻撃者バケットへ<br/>モデルをアップロード"]
C --> D["Cloud Functionが<br/>object finalizeで反応"]
D --> E["joblibモデルを<br/>悪意あるファイルへ差し替え"]
E --> F["Vertex AIサービスエージェントが<br/>差し替え後を読み取り"]
F --> G["デプロイ時にpickle/joblib<br/>デシリアライズでコード実行"]
style G fill:#991b1b,color:#fff
RCEの実行点は、モデルサービング側のPythonデシリアライズだ。
pickleやjoblibは、読み込み時に任意コード実行へつながる仕組みを持つ。
機械学習ではモデル成果物として普通に使われるが、信頼できないファイルを joblib.load() / pickle.load() する設計は、その時点でコード実行を許す。
漏れたトークンで別モデル成果物やログ情報まで読めた
Unit 42は、差し替えたモデルからGoogle Compute Engineのメタデータサーバーへアクセスし、サービングコンテナのサービスアカウントOAuthトークンを外部へ送るペイロード(悪意あるコード本体)を試している。
そのトークンでは、Unit 42の検証環境で単一の被害者デプロイを越えて周辺リソースを読めた。
確認された範囲は広い。 同じGoogle管理テナントプロジェクト内では、別デプロイのCloud Storageバケットと完全なTensorFlowモデル成果物を読めた。被害者プロジェクト側では、BigQueryデータセット名、テーブル名、アクセス制御リストを列挙できた。さらにGoogle管理テナント側のCloud Loggingから、GKEクラスタ名、稼働中の予測デプロイ、Google内部コンテナイメージURI、KubernetesシステムIDまで出てきた。
差し替え自体は「自分のモデルが置き換わる」で済む話だが、差し替えたモデルが得たサービスアカウントトークンで、デプロイ単位を越えた情報まで届いた。
開発・運用の便利な経路が認証情報につながる例としては、前に書いたVitest APIサーバーのWebSocketからローカル開発端末のRCEにつながるCVE-2025-24964がある。
Vitestはローカルの開発サーバー、今回のVertex AIはクラウドSDKのステージング処理で対象は違うが、どちらも本番サービス本体ではなく周辺の開発・登録フローから権限を奪われた。
Miasma系の記事でも、GitHub・npm・Azure・GCP・AWS・Kubernetesの認証情報が開発端末やCIランナーに集まる前提で見た。
Microsoft系73リポジトリ停止のMiasmaはソースリポジトリを開く操作が入口だった。
今回のVertex AIは、モデルアップロードの一時バケットが入口になる。
1.148.0で所有者確認まで入る
Googleの修正は2段階だった。
Unit 42によると、2026年3月31日の1.144.0でステージングバケット名に uuid4 由来のsaltが入った。
その後、2026年4月15日の1.148.0で Model.upload() にバケット所有者確認が入った。
GitHubの python-aiplatform changelogにも、1.148.0のBug Fixesとして Model.upload() のbucket ownership verificationが記録されている。
確認する版は1.148.0以降だ。
単に「推測しにくい名前になった」だけでなく、既存バケットを使うときに所有者を確認する修正まで含める。
python -m pip show google-cloud-aiplatform
python -m pip freeze | grep google-cloud-aiplatform
ノートブックでは、ローカルの仮想環境とカーネルが違う版を読んでいることがある。
Jupyter / Colab Enterprise / Workbench / CI / 学習ジョブのベースイメージで、それぞれ google-cloud-aiplatform のバージョンを確認する。
requirements.txt だけではなく、実行時の環境で確認する。
staging_bucket は自分で作って明示する
SDK更新に加えて、モデルアップロード時は自分の管理するCloud Storageバケットを staging_bucket に指定する。
ローカルディレクトリを artifact_uri にしてSDKへ渡す場合でも、ステージング先はCloud Storageになる。
その一時置き場が自分のプロジェクト・自分のIAMで管理されているかを固定する。
from google.cloud import aiplatform
aiplatform.init(
project="my-project",
location="us-central1",
staging_bucket="gs://my-project-vertex-staging-us-central1",
)
model = aiplatform.Model.upload(
display_name="my-model",
artifact_uri="local_model_dir",
serving_container_image_uri="us-docker.pkg.dev/vertex-ai/prediction/sklearn-cpu.1-0:latest",
)
このバケットはTerraformなどで先に作り、Uniform bucket-level access、Public Access Prevention、最小限のIAM、ログを付けた状態で管理する。
「SDKがなければ作る」に任せるより、環境構築側で名前・所有者・権限を固定するほうが事故後にも追いやすい。
CVE-2026-2473と同じ名前生成パターンを見る
Googleのセキュリティ情報には、2026年2月20日公開のGCP-2026-012としてCVE-2026-2473が載っている。
こちらはVertex AI Experiments 1.21.0以上1.133.0未満の予測可能なバケット名問題で、事前作成されたCloud StorageバケットからクロステナントRCE(別テナント境界をまたぐリモートコード実行)、モデル窃取、モデル汚染(攻撃者による成果物差し替え)につながる内容だった。
Googleは1.133.0以降で緩和済みと書いている。
今回の Model.upload() 問題はCVE-2026-2473そのものではない。
ただ、名前生成、存在確認、所有者確認の抜け、Google管理テナント側への到達という確認点は近い。
SDK版として google-cloud-aiplatform が1.148.0以降か、Vertex AI Experimentsを使う環境では1.133.0未満が残っていないかを確認する。コード上では Model.upload()、aiplatform.init()、パイプライン定義で staging_bucket を省略していないかを見る。さらにCloud Storage上で、PROJECT-vertex-staging-REGION 形式や古い既定名に、所有者不明のバケット、広すぎるIAM、ログのない一時バケットがないか確認する。
公開情報では、この Model.upload() 問題の実悪用は確認されていない。
それでも、脆弱なSDKがノートブックやCIに残ると、同じコードが新リージョン・新プロジェクトで再実行されたタイミングに条件が揃う。