新しい入口に認可を足しても、古い入口が残っていれば素通しになる

認可の実装で最も踏みやすい失敗は、認可そのものの実装を間違えることではない。新しい入口に正しく認可を付けたのに、古い入口を閉じ忘れるほうだ。

新しい入口の認可は、コードレビューでもテストでも念入りに見られる。だが古い入口は「前からあるもの」なので誰も見ない。結果として、認可は確かに実装されているのに、認可を通らない経路が同じプロセスの中に生き残る。

Google の MCP Toolbox for Databases(以下 mcp-toolbox)に付いた CVE-2026-14537 は、まさにこの形だった。この記事では、NVD の記述と実際のパッチを突き合わせて、何が起きていたのか、そしてなぜ修正が「認可の穴を塞ぐ」ではなく「その構成では起動させない」になったのかを読む。

掲載は 脆弱性対応ウォッチ にもある。

何が起きていたか

NVD の記述はこうである。

--enable-api フラグが有効なとき、レガシー HTTP エンドポイント経由でツール呼び出しリクエストを送ることにより、未認証の攻撃者が scopeRequired 機能で保護されたツールを呼び出せる。

登場するものを整理する。

  • scopeRequired: ツールごとに「この OAuth スコープを持っていないと呼べない」を宣言する機能。mcp-toolbox の認可の中心にある。
  • --enable-api: レガシーの HTTP API を有効にするフラグ。既定では無効。
  • レガシー HTTP エンドポイント: MCP プロトコル以前からある、直接 HTTP でツールを呼ぶための経路。

つまり、scopeRequired による保護は MCP 側の経路にしか掛かっていなかった。同じツールをレガシー HTTP API から呼ぶと、スコープの検査を通らずに実行できた。

深刻度の評価は割れている。

採番元 基本値 ベクタ
Google CVSS 4.0 8.1 AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:HE:U
NVD CVSS 3.1 9.8 (Primary) AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Google 側のベクタにある E:U は Exploit Maturity が Unreported、つまり悪用の報告が無いという評価である。CWE は CWE-863(不正な認可)。CISA の KEV には、本稿の確認時点(2026-08-24・カタログ 2026.08.21)で収載されていない。

対象バージョンは v1.3.0 と v1.4.0 の2つだけである。NVD の CPE も、範囲ではなくこの2つをピンポイントで指定している。

修正のパッチを読む

修正は upstream の PR #3435 として入った。タイトルは次のとおりである。

1
fix(server): fail if MCP auth is enabled together with enable-api

タイトルの時点で、これが認可の修正ではないことが分かる。「MCP 認可が enable-api と同時に有効なら失敗させる」と書いてある。

PR の説明はこうである。

MCP Authorization が有効な状態でレガシー HTTP API(--enable-api / EnableAPI)と同時にサーバーを起動することを防ぐ、起動時の検証チェックを導入する。レガシーエンドポイントは非推奨であり、新機能はサポートされないため、レガシー HTTP API が有効な場合、クライアントが MCP の認可ポリシーを回避しうる。

マージは 2026年6月18日、同日リリースの v1.5.0 に含まれている。変更は9ファイル、127行追加・13行削除である。

中心の変更は cmd/root.go にある。修正前は、MCP 認可が有効なときに ToolboxUrl の有無だけを検査していた。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 修正前
for _, authSvc := range opts.Cfg.AuthServiceConfigs {
if genCfg, ok := authSvc.(generic.Config); ok && genCfg.McpEnabled {
if opts.Cfg.ToolboxUrl == "" {
opts.Cfg.ToolboxUrl = os.Getenv("TOOLBOX_URL")
}
if opts.Cfg.ToolboxUrl == "" {
errMsg := fmt.Errorf("MCP Auth is enabled but Toolbox URL is missing. ...")
opts.Logger.ErrorContext(ctx, errMsg.Error())
return errMsg
}
break
}
}

修正後は、まず「MCP 認可が有効か」をフラグに落とし、有効なら EnableAPI との同時指定を先に弾く

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 修正後
var mcpAuthEnabled bool
for _, authSvc := range opts.Cfg.AuthServiceConfigs {
if authSvc.IsMCPEnabled() {
mcpAuthEnabled = true
break
}
}

if mcpAuthEnabled {
if opts.Cfg.EnableAPI {
errMsg := fmt.Errorf("MCP Auth cannot be enabled together with the legacy HTTP API (--enable-api)")
opts.Logger.ErrorContext(ctx, errMsg.Error())
return errMsg
}
if opts.Cfg.ToolboxUrl == "" {
opts.Cfg.ToolboxUrl = os.Getenv("TOOLBOX_URL")
}
if opts.Cfg.ToolboxUrl == "" {
errMsg := fmt.Errorf("MCP Auth is enabled but Toolbox URL is missing. ...")
opts.Logger.ErrorContext(ctx, errMsg.Error())
return errMsg
}
}

読み取れることが3つある。

1つ目。判定が型アサーションからインターフェースのメソッドに変わった。 修正前は authSvc.(generic.Config) という型アサーションで、generic.Config の場合しか MCP 有効を検出できなかった。修正後は authSvc.IsMCPEnabled() を呼ぶ。実際、同じ PR で internal/auth/auth.go にメソッドが1行足され、internal/auth/generic/generic.gointernal/auth/google/google.go にそれぞれ4行の実装が入っている。つまり google の認可サービスを使っていた場合、修正前は MCP 有効の検出そのものが漏れていたToolboxUrl の必須検査も同時に効いていなかったことになる。

2つ目。同じ検査が internal/server/server.go にも重複して入っている。

1
2
3
4
5
6
7
8
if cfg.EnableAPI {
for _, sc := range cfg.AuthServiceConfigs {
if sc.IsMCPEnabled() {
return nil, nil, nil, nil, nil, nil, nil,
fmt.Errorf("MCP Auth cannot be enabled together with the legacy HTTP API (EnableAPI)")
}
}
}

cmd/root.go の検査は CLI から起動したときにしか通らない。mcp-toolbox をライブラリとして組み込み、server.InitializeConfigs を直接呼ぶ経路では素通りする。入口が2つあるなら検査も2つ要る、という判断である。この PR は、まさに「入口が複数ある」ことで起きた脆弱性を直しているので、修正自体が同じ罠を踏まないようにした形になっている。

3つ目。テストが本体より多い。 9ファイルのうち4つがテストで、cmd/root_test.go に29行、internal/server/server_test.go に54行が足されている。本体の追加が実質30行程度なので、テストのほうが厚い。禁止する構成を将来うっかり許可し直すことがないよう固定してある。

なぜ「認可を足す」ではなく「起動させない」なのか

素朴に考えれば、レガシー HTTP エンドポイント側にも scopeRequired の検査を足せばよさそうに見える。実際にはそうしていない。理由は PR の説明文に書いてある。「レガシーエンドポイントは非推奨であり、新機能はサポートされない」からである。

これは設計判断として筋が通っている。整理するとこうなる。

  • 非推奨の経路に認可を足すと、その経路を維持する約束が生まれる。 認可は一度実装したら終わりではない。スコープの体系が変われば追随が要る。新しいツール種別が増えれば、そこにも検査が要る。非推奨の経路にそれを背負わせると、消すつもりの経路が消せなくなる。
  • 同じ検査を2箇所に持つと、いつか片方だけ古くなる。 今回の脆弱性そのものが「片方にしか無かった」ことで起きている。両方に置いても、次は「片方が古い」で同じことが起きる。
  • 構成を禁止すれば、検査は1箇所で済む。 MCP 認可とレガシー API が同時に有効になることが無いなら、レガシー側に認可が無くても認可の回避は起こらない。

つまりこの修正は、危険な状態を検知するのではなく、危険な状態を作れなくしている。fail-closed の考え方をそのまま適用した形である。

副作用として、v1.5.0 以降にバージョンを上げると、両方を有効にしていた構成は起動しなくなる。エラーメッセージは MCP Auth cannot be enabled together with the legacy HTTP API (--enable-api) である。これは意図された挙動なので、上げたあとに起動しなくなったら、レガシー API を切るか、MCP 認可を切るかを選ぶことになる。切るべきはレガシー API のほうである。

自分の環境が該当するかを調べる

該当するのは、次の3つがすべて成り立つときだけである。切り分けは速い。

1. バージョンが v1.3.0 または v1.4.0 か。

1
toolbox --version

v1.5.0 以降なら、そもそもこの構成では起動しないので該当しない。v1.2.0 以前は NVD の CPE の対象外である。

2. --enable-api を付けているか。 既定では無効なので、明示的に付けていなければ攻撃経路は開いていない。systemd の unit ファイル、Dockerfile の CMD、Kubernetes の Deployment の args、docker-compose の command を実際に見る。まとめて掃くならこうなる。

1
2
3
4
5
6
7
# 起動定義から --enable-api を探す
grep -rn -- "--enable-api\|EnableAPI" \
/etc/systemd/system \
./Dockerfile ./docker-compose.yml ./k8s 2>/dev/null

# 稼働中のプロセスの引数を直接見る(こちらが確実)
ps -eo args | grep '[t]oolbox'

設定ファイルではなく実際に動いているプロセスの引数を見るのが確実である。イメージのタグを固定していると、リポジトリの定義と本番が食い違っていることがある。

3. scopeRequired を使っているか。 使っているツールが何をするものかで優先度が決まる。

1
2
# tools.yaml から scopeRequired を持つツールを拾う
grep -n -B4 "scopeRequired" tools.yaml

データベースへの書き込みや削除を行うツールが含まれているなら優先度は高い。参照だけなら影響は情報漏えいにとどまる。

対応は v1.5.0 以降へ上げることである。すぐに上げられない場合の当面の措置は --enable-api を外すことで、これは成立条件そのものを消すので確実である。ただし公式の workaround として提示されたものではない。

手元のコードで同じ形を探す

この脆弱性の形は mcp-toolbox 固有ではない。「新旧2つの入口があり、認可が新しいほうにしか無い」は、次のような場面で普通に起こる。

  • REST API を GraphQL や gRPC に移行中で、旧 REST が残っている
  • 管理画面の操作を API 化したが、旧来の内部エンドポイントも生きている
  • 認証を新しいミドルウェアに移したが、旧ルーターにぶら下がったハンドラが残っている

探し方は、認可の実装からではなくルーティングの登録から見るのが速い。認可のコードを読むと「ちゃんと実装されている」としか分からないためだ。

1
2
3
# ルート登録を全部列挙してから、認可ミドルウェアの適用漏れを見る(Go の例)
grep -rn "HandleFunc\|router.Handle\|mux.Handle" --include="*.go" . \
| grep -v "_test.go"

そのうえで、次の2つを確認する。

  1. 登録されている全ルートが、認可ミドルウェアの下にぶら下がっているか。 ミドルウェアをサブルーターにだけ適用していて、親に直接登録されたハンドラが素通しになっている、という形が最も多い。
  2. 同じ機能に到達する経路が2つ以上無いか。 あるなら、片方を消すか、今回の修正のように同時に有効にできなくする

そして今回の修正から借りるべき考え方は、検査を増やすことではなく、危険な組み合わせを構成の段階で作れなくすることである。実行時に検知して拒否する方式は、検知漏れの余地が残る。起動時に落とす方式は、そもそも動いていない。

まとめ

  • CVE-2026-14537 は、mcp-toolbox v1.3.0 / v1.4.0 で、--enable-api が有効なとき scopeRequired の認可をレガシー HTTP API から回避できた問題である。
  • NVD の Primary 評価は CVSS 3.1 で 9.8、採番元の Google は CVSS 4.0 で 8.1E:U)。KEV には未収載(2026-08-24 時点)。
  • 修正(PR #3435・2026年6月18日マージ・v1.5.0)は、レガシー側に認可を足すのではなく、MCP 認可とレガシー API の同時有効化を起動時に禁止するというものだった。
  • 同じ検査が cmd/root.gointernal/server/server.go の2箇所に入っている。CLI 経由とライブラリ経由という2つの入口があるためである。
  • 該当条件は「v1.3.0 か v1.4.0」「--enable-api を付けている」「scopeRequired を使っている」の3つすべて。1つでも外れれば該当しない。

本記事は 2026年8月24日に、NVD の API 応答(vulnStatus: AnalyzedlastModified 2026-08-08)、GitHub の PR #3435 の本文と差分、CISA KEV カタログ 2026.08.21 を実際に取得して確認した内容にもとづく。バージョンや評価は更新されることがあるので、対応の前に一次情報を確認してほしい。


新しい入口に認可を足しても、古い入口が残っていれば素通しになる
https://blog.hashito.biz/2026/08/24/mcp-toolbox-cve-2026-14537-legacy-endpoint-authz-bypass/
著者
hashito
作成日
2026年8月24日
著作権