3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Gemini Canvas で API キーなしでも Gemini API が動く謎 ── Claude と一緒にソースコードを読んで解明した

3
Posted at

🏁 結論

Canvas で Gemini API に対してリクエストをすると、Gemini API のエンドポイントではなく
Gemini の Web UI でチャットするときと同じエンドポイントに対してリクエストが飛ぶ。

だから、Canvas 上で動かすアプリケーション内で Gemini API を利用する実装をしていたら
API キーがなくてもレスポンスが返ってくる。

注意
本記事の動作は 2026/04/08 時点で確認されたものです。

🪧 はじめに

社内の Slack で以下の動画が共有されて「これよくない?」と主にビジネスサイドのメンバー内で話題になっていた。

【有料級】「AI-OCR製品」をGeminiのCanvasで表現しよう!

タイトルとサムネイルを見て、いくつかの懸念が頭をよぎった。

  • 「Canvas 上の Web アプリで Gemini API を使って画像認識させるなら、API キーが必要なはず」
    • 「もし動画の中で Google AI Studio で API キー発行する的な手順が紹介されていたら、管理されていない "野良 API キー" が増えてしまう」
  • 「まだ社内で非エンジニアが Gemini API キーを使うためのポリシーが決まっていないから推奨できない」
  • 「しかも無料枠を超えると従量課金になるから、予算管理の問題が出てくる」

しかし実際に動画を見たところ、驚きの一言が。

「Canvas 内であれば Gemini API が API キーなしで使えるようになっている」

「…そんなことある?」

試しに自分でも動画で紹介されているソースコードを Canvas で描画して動かしてみると、たしかに動いてしまった。
ソースコードを覗いてみると、以下のようになっていた。

const apiKey = ""; // System provides this

たしかに空文字になっている。

通常、API キーが空のまま Gemini API にリクエストを投げたら「 API キーがないよ」と怒られるはず。
なのに AI による画像認識が普通に成功している。

なにが起きているかわからないと「やって問題ない」とも「やってはダメ」とも言えないと考えて
まず「何が起きているのか」を正しく理解する必要があると思い、Claude と一緒に調べることにした。

🔍 事象の確認:DevTools で何が起きているか見てみた

最初にブラウザの DevTools を開いて、実際に何が送信されているか確認した。

Network タブ:通信先がすり替わっていた

Network タブでリクエストを見ると、予想と全く異なる宛先が表示された。

項目 コードに書かれた値 実際に送信された値
:authority(宛先ホスト) generativelanguage.googleapis.com gemini.google.com
:path(宛先パス) /v1beta/models/.../generateContent /_/BardChatUi/data/batchexecute?...
content-type application/json application/x-www-form-urlencoded

コードに書いた宛先(外部開発者向けの Gemini API)ではなく、gemini.google.com の内部通信エンドポイントbatchexecute)に送られていた。つまり、ブラウザから Gemini のチャット画面を使うときとまったく同じ経路ということ。しかも cookie ヘッダーには自分の Google アカウントのセッション情報(SID, HSID, __Secure-1PSID など)が自動的に付与されていた。

システムがリクエストをどこかで横取りして、別の宛先に投げ直している
そう仮説を立てて、次は Console のスタックトレースを確認した。

Console タブ:fetch が BardChatUI のコードから呼ばれていた

スタックトレースには次のような記録が残っていた。

fetch @ https://www.gstatic.com/_/mss/boq-bard-web/_/js/k=boq-bard-web.BardChatUi...
invokeTask
scheduleTask
...

コード内の fetch が、Canvas 内の自分がコピペしたコードから直接呼ばれているのではなく、Gemini の Web UI 基盤(BardChatUi)のコードを経由して呼ばれていることがわかった。

「Service Worker か、window.fetch の上書き(Monkey Patch)のどちらかだろう」と当初は予想した。しかし Claude と一緒に DevTools の Application → Service Workers を確認したところ、gemini.google.comService Worker は登録されていなかったwindow.fetch の直接上書きも存在しない。

「では、どうやって横取りしているのか?」
ここからソースコードを読み解いていくことにした。

🌱 種明かし:ソースコードを読んで仕組みを解明した

www.gstatic.com から配信されている BardChatUI の minified JS(boq-bard-web.BardChatUi...)を DevTools の Sources タブで直接読み、インターセプトの実装を特定した。

全体像:iframe と postMessage によるメッセージパッシング

答えは iframe と postMessage だった。
Canvas 内のコードが fetch で Gemini API を呼んでも、実際には以下の流れに差し替えられている。

image.png

つまり何が起きているか:

  • generativelanguage.googleapis.com(Gemini API)へのリクエストは一切発生しない
  • ユーザーのコードには fetch で API を叩くと書いてあっても、裏では Gemini Web UI の内部エンドポイントに完全置換されている
  • Canvas はサンドボックスの中から直接 API を叩けず、必ず親ページを経由する構造になっている

処理の流れを図にするとこうなる。
image.png

ソースコードで確認できた各コンポーネント

_.XMb:インターセプト対象のドメイン定義

_.XMb = new _.$v(
  "45727800",
  (0, _.$z)('[["generativelanguage.googleapis.com"]]'),
  _.Zz
);

「どのドメインへの fetch をインターセプトするか」の設定値。generativelanguage.googleapis.com が明示的に登録されている。

WIZ_global_data["eptZe"]:batchexecute URL の動的解決

window.WIZ_global_data = {
  "eptZe": "/_/BardChatUi/",
  // ...
}

ページロード時にサーバーが動的に注入するグローバル設定。Canvas のコンテキストでは "/_/BardChatUi/" が入っており、これをもとに最終的なリクエスト先 URL が組み立てられる。

// Qza 関数(URL 組み立て)
var u = _.Rx(
  a.l,
  "" + _.bi("eptZe") + "data/batchexecute",
  // → "/_/BardChatUi/data/batchexecute"
);

y5d / "q4uTj":Canvas 向け RPC メソッド ID

var y5d = new _.Wz(
  "q4uTj",  // Canvas API 呼び出し専用の rpcid
  class extends _.l { ... }
);

"q4uTj" は Canvas 向けの API リクエストを処理する RPC メソッド ID。Network タブで観測されたリクエストの rpcids パラメータと一致した。

_.HM.Nb():インターセプト処理の本体

最も重要なコンポーネント。_.HM クラスは親ページの Angular コンポーネントで、コンストラクタで window.addEventListener("message", ...) を登録し、iframe からの postMessage を常時監視している。

_.HM = class {
  Nb(a) {  // postMessage イベントハンドラ
    if (
      a.data?.type === "requestFetch" &&
      typeof a.data.url === "string" &&
      typeof a.data.modelName === "string" &&
      typeof a.data.promiseId === "number"
    ) {
      const t = a.data.promiseId;
      this.Ba.lYa({          // batchexecute へ実際にリクエスト
        bK: a.data.modelName,
        LUb: a.data.options.body,
        U0b: a.data.url.includes("streamGenerateContent")
      }).subscribe(v => {
        this.ha.sendMessage({ // iframe に結果を返却
          type: "resolveFetch",
          promiseId: t,
          response: { status: 200, body: v }
        })
      })
    }
  }
  constructor() {
    this.qb = this.va.Aa(_.XMb); // インターセプト対象ドメインを読み込み
    window.addEventListener("message", c => { this.Nb(c) })
  }
}

🐞 補足

batchexecute とは何か

今回のリクエスト先として登場した batchexecute について補足。

これは YouTube、Google Play、Google Maps、Gemini など、Google の多くの Web アプリのフロントエンドとバックエンド間通信に使われている汎用 RPC エンドポイントです。REST API のように機能ごとに URL を分けるのではなく、通信を 1 つの URL に集約し、ペイロードの内容で処理を指定する設計になっています。

送受信データは JSPB(Protobuf のスキーマを JSON 配列に対応付けた Google 独自形式)と呼ばれる多重配列で、一般の開発者が扱う REST API の JSON とは互換性がない。Canvas のシステムはユーザーの JSON リクエストを JSPB に変換し、レスポンスも逆変換してユーザーのコードに返しているようです。

公式ドキュメントに記述なし

公式ドキュメントに本件に関する記述を探してみたが見つけることはできませんでした。

『Canvas で API キーなしでも Gemini API が動く (ように見える)』の動作については
公式ドキュメントに記載のない非公開仕様を前提とした動作であることは念頭に置いておく必要があります。

(ただ自分が情報ソースを見つけられてないだけ説もありますが...)

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?