AWS MCPサーバー群の中核である「AWS API MCP Server」に、設定したはずのセキュリティポリシーが黙って無効になる脆弱性 CVE-2026-16584 が公表された。CVSS 7.0(high)だが、この脆弱性の本質はスコアではなく「失敗したときにどちらへ倒れるか」という設計判断にある。起動時にAWSへのHTTPリクエストが1回失敗するだけで、拒否リストの判定がプロセスの寿命が尽きるまでスキップされる。しかもエージェント側から見た挙動は完全に正常なままだ。本記事では、公開されている修正差分とパッケージの実物を読み、アドバイザリが書いていない「どの設定が素通りし、どの設定は無事だったのか」まで切り分ける。
まず自分の環境が該当するかを確認する。バージョン判定と、実際に発火したかどうかの判定は別物なので両方見る。
# 1) 導入バージョンを確認(1.3.47未満なら該当)
pip show awslabs.aws-api-mcp-server | grep -i version
uv pip show awslabs.aws-api-mcp-server 2>/dev/null | grep -i version
# 2) 実際にポリシーが飛んだかをログで確認(この警告行が出ていたら発火している)
# ログは ~/.aws/aws-api-mcp/aws-api-mcp-server.log(保持7日・10MBでローテート)
grep "Failed to load read operations index" ~/.aws/aws-api-mcp/aws-api-mcp-server.log*
# 3) 拒否リストを設定していたかを確認(設定していた人が最も影響が大きい)
cat ~/.aws/aws-api-mcp/mcp-security-policy.json 2>/dev/null || echo "ポリシーファイルなし"
# 4) 起動時に叩かれるエンドポイントへ到達できるか(1.3.47以降は到達できないと起動しない)
curl -s -o /dev/null -w "status=%{http_code} time=%{time_total}s\n" \
--max-time 5 https://servicereference.us-east-1.amazonaws.com/
- ・何が起きた:起動時の初期化に失敗すると、リクエストごとのセキュリティポリシー判定がプロセスの寿命の間ずっとスキップされる(CWE-455 / Non-exit on Failed Initialization)。
- ・対象:
awslabs.aws-api-mcp-serverの 0.2.13以上 1.3.47未満。修正版は 1.3.47。 - ・素通りするもの:
mcp-security-policy.jsonの denyList / elicitList。ここには代替経路が一切なかった。 - ・無事だったもの:環境変数
READ_OPERATIONS_ONLYとREQUIRE_MUTATION_CONSENT。代替経路が残っており、むしろ通常時より厳しく倒れていた。 - ・引き金:起動時に1回だけ走るタイムアウト5秒のHTTP GET。攻撃者が任意に起こせるものではなく、ネットワークの不調で起きる。
- ・上げるときの注意:1.3.47以降は同じ失敗で起動しなくなる。キャッシュも再試行も追加されていない。
開発者を狙う供給網まわりの攻撃と防御の全体像は サプライチェーンセキュリティ2026|攻撃手法・防御ツール・実践チェックリスト をご覧ください。
CVE-2026-16584とは:AWS MCPサーバーの許可判定が失敗時に消える
AWS API MCP Serverは、AIアシスタントに AWS CLIコマンドの実行能力そのものを与えるMCPサーバーだ。call_aws というツールを通じて、エージェントが組み立てたAWS CLIコマンドをローカルで実行する。エージェントにAWSアカウントの操作権限を渡す以上、「何を実行させないか」を決める仕組みが要る。それがオプションのセキュリティポリシーで、~/.aws/aws-api-mcp/mcp-security-policy.json に拒否リスト(denyList)と同意要求リスト(elicitList)を書く。
CVE-2026-16584は、このポリシー判定そのものが消えるという脆弱性である。
| 項目 | 値 |
|---|---|
| CVE番号 | CVE-2026-16584 |
| アドバイザリ | GHSA-29w2-fq35-v728 |
| 対象パッケージ | awslabs.aws-api-mcp-server(PyPI・Apache-2.0) |
| リポジトリ | awslabs/mcp(30以上のAWS製MCPサーバを収めたモノレポ) |
| 影響バージョン | >= 0.2.13, < 1.3.47 |
| 修正バージョン | 1.3.47 |
| 深刻度 | high / CVSS v3.1 7.0(AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H) |
| CVSS v4.0 | 7.3(AV:L/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N) |
| CWE | CWE-455(Non-exit on Failed Initialization) |
| EPSS | 0.13%(パーセンタイル3.0) |
| 報告者 | Lav Kumar Vishwakarma(独立研究者) |
| AWS公式速報 | security bulletin 2026-063-aws |
CVSSベクタが AV:L(ローカル)・AC:H(攻撃条件が複雑)・UI:R(利用者の操作が必要)である点は、この脆弱性の性格をよく表している。攻撃者がリモートから任意に引き起こせる類のものではない。EPSS(30日以内に悪用が観測される確率の推定値)が0.13%・下位3パーセンタイルに留まっているのも同じ理由だ。
なお、awslabs/mcp のスター数(約9,507)はモノレポ全体の数字であり、このサーバー単体の利用規模を示すものではない。同じパッケージには2026年3月にも別のバイパス脆弱性が公表されており、今回が初めてではない。
| CVE | 内容 | CWE | CVSS | 影響範囲 | 公表 |
|---|---|---|---|---|---|
| CVE-2026-4270 | ファイルアクセス制限のバイパス | CWE-424 | 5.5 | >=0.2.14, <1.3.9 |
2026-03-17 |
| CVE-2026-16584 | セキュリティポリシーのバイパス | CWE-455 | 7.0 | >=0.2.13, <1.3.47 |
2026-07-24 |
どちらも「制限をかけたつもりの機構が回避される」という同じ性質の問題である点は、傾向として押さえておく価値がある。
パッチが先、アドバイザリが後だった
タイムラインを時刻まで並べると、公開の順序に特徴がある。修正版1.3.47がPyPIに出たのはJST 2026-07-22 23:26、NVDへの登録が07-24 01:17、GHSAの公開は07-25 07:33だった。修正の提供からアドバイザリ公開まで、約2日半のずれがある。
これは調整された脆弱性開示では珍しくない順序だが、利用者側には実務的な意味がある。DependabotやRenovateといった依存関係スキャナは、多くがGHSA/NVDのデータベースを参照して警告を出す。アドバイザリが公開されるまでの約2日半、修正版は存在していたのに自動では通知されない状態だったことになる。
さらにこの間の07-23には1.4.0が公開されている。つまり uvx ...@latest のように常に最新を取得する構成であれば、警告を受け取る前に自動的に修正済みバージョンへ移っていた可能性が高い。逆にバージョンを固定していた環境では、GHSA公開まで気づく手段がなかった。
何が起きていたのか:1.3.46の実コードが示す挙動
ここからは推測ではなく、PyPIで配布されている実物のソースを読む。脆弱なバージョン(1.3.46)と修正版(1.3.47)のsdistを取得して差分を取ると、原因は server.py の2か所に集約されている。
起動処理側は、こう書かれていた。
# awslabs/aws_api_mcp_server/server.py(1.3.46・脆弱)
try:
READ_OPERATIONS_INDEX = get_read_only_operations()
except Exception as e:
logger.warning('Failed to load read operations index: {}', e)
READ_OPERATIONS_INDEX = None # ← 失敗しても None を入れて起動を続行する
例外を握りつぶし、warning を1行出して、None を代入したうえでサーバーは起動する。この READ_OPERATIONS_INDEX が、リクエストごとの判定で使われる。
get_read_only_operations()"] --> Q{"読み込み成功?"} Q -- "成功" --> OK["READ_OPERATIONS_INDEX = 索引"] Q -- "失敗" --> NG["warning を1行出して
READ_OPERATIONS_INDEX = None"] OK --> R1["リクエスト: check_security_policy() を実行
denyList / elicitList / 環境変数を評価"] NG --> R2["リクエスト: check_security_policy() を呼ばない
環境変数のみの代替経路へ"] R2 --> D{"読み取り専用モード
または変更時同意要求
の環境変数が有効?"} D -- "はい" --> BLK["拒否 / 同意要求
(防御は残る)"] D -- "いいえ(既定)" --> PASS["そのまま実行
denyList は評価されない"]
リクエスト側の分岐が、この脆弱性の核心だ。1.3.46では次のようになっていた。
# 1.3.46(脆弱)— リクエストごとの判定
if READ_OPERATIONS_INDEX is not None:
policy_decision = check_security_policy(ir, READ_OPERATIONS_INDEX, ctx)
if policy_decision == PolicyDecision.DENY:
raise AwsApiMcpError('Execution of this operation is denied by security policy.')
elif policy_decision == PolicyDecision.ELICIT:
await request_consent(cli_command, ctx)
else:
# 索引が無いときの代替経路(環境変数だけを見る)
if READ_OPERATIONS_ONLY_MODE:
raise AwsApiMcpError('... read only mode is enabled ...')
elif REQUIRE_MUTATION_CONSENT:
await request_consent(cli_command, ctx)
else に落ちたとき、check_security_policy() は一度も呼ばれない。denyListもelicitListも、この経路には存在しない。
影響範囲:素通りしたのはJSONで書いたポリシーだけ
アドバイザリは「既定の構成ではリクエストごとのセキュリティチェックがバイパスされる」と書いている。この「既定の構成」という限定は重要で、すべての防御が一律に消えたわけではない。上の else ブロックは何もしない空処理ではなく、2つの環境変数だけは見ていたからだ。
そして core/common/config.py を読むと、その2つはどちらも既定値がFalseである。
READ_OPERATIONS_ONLY_MODE = get_env_bool(READ_ONLY_KEY, False) # READ_OPERATIONS_ONLY
REQUIRE_MUTATION_CONSENT = get_env_bool(REQUIRE_MUTATION_CONSENT_KEY, False)
つまり何も環境変数を設定していない既定状態では、else 経路は実質的に素通りになる。これがアドバイザリの言う「既定の構成」の正体だ。設定ごとに整理すると次のようになる。
| 設定していたもの | 索引の読み込みに失敗したとき | 通常時と比べて |
|---|---|---|
mcp-security-policy.json の denyList |
判定されない(代替経路に存在しない) | 完全に失われる |
mcp-security-policy.json の elicitList |
判定されない(同上) | 完全に失われる |
環境変数 READ_OPERATIONS_ONLY=true |
全操作を一律で拒否 | むしろ厳しい |
環境変数 REQUIRE_MUTATION_CONSENT=true |
全操作で同意を要求 | むしろ厳しい |
| 何も設定していない(既定) | 制限なしのまま | 変わらない(元から制限なし) |
環境変数の2つが「むしろ厳しい」側に倒れるのは、索引が無いとその操作が読み取り専用かどうかを判定できないからだ。通常の判定関数 determine_policy_effect() は if READ_OPERATIONS_ONLY_MODE and not is_read_only: DENY と書かれていて、読み取り操作は通す。一方 else 経路は is_read_only を知りようがないので、区別せず全部を拒否する。安全側ではあるが、利用者から見れば「読み取りコマンドまで急に拒否されるようになった」という別の症状として現れる。
① 何が起きる:起動時のデータ読み込みが1回失敗すると、denyList/elicitListの判定がプロセスの寿命の間スキップされる。② 何を解決する必要がある:1.3.47以上へ更新する。ただし更新すると同じ失敗で起動しなくなるため、到達性の確認が先。③ 何が守られていたか:IAM権限は一切影響を受けない。環境変数ベースの2つの制限も残っていた。失われたのはJSONで書いたポリシーだけ。
そもそも「読み取り専用かどうか」はどう判定されているのか
素通りの範囲を正しく理解するには、失われた READ_OPERATIONS_INDEX が何をしていたのかを知る必要がある。これは単なるフラグではなく、どのAWS操作が読み取り専用かを引くための索引だ。判定は3つの情報源を重ねて行われる。
・パッケージに同梱された data/api_metadata.json(type: ReadOnly の操作を抽出)
・AWSのサービスリファレンス文書(IsWrite が偽の操作を、サービス単位で遅延取得してキャッシュ)
・コード内にハードコードされた上書きリスト(OVERRIDES)
3つ目の上書きリストが示唆に富む。API名としては「取得(Get)」に見えるが実質的に権限を生む操作を、明示的に読み取り専用から除外しているのだ。
| サービス | 操作 | 読み取り専用として扱うか |
|---|---|---|
sts |
AssumeRole / AssumeRoleWithWebIdentity / AssumeRoleWithSAML / GetSessionToken / GetFederationToken / AssumeRoot |
いいえ |
iam |
CreateAccessKey |
いいえ |
cognito-identity |
GetCredentialsForIdentity / GetOpenIdToken |
いいえ |
sso |
GetRoleCredentials |
いいえ |
つまり「読み取り専用モードなら安全」という素朴な期待に対して、認証情報を発行する操作は書き込み扱いにするという判断が明示的に組み込まれている。逆に言えば、この索引が失われた状態は「読み取り専用の定義そのものを失った状態」であり、だからこそ代替経路は全操作を一律で拒否するしかなかった。
IAM権限は最後の砦として残る
アドバイザリが明記しているとおり、影響範囲はセキュリティポリシーのゲートに限定される。サーバーに渡したAWS認証情報のIAM権限はそのまま効き続け、サーバーができることの上限を規定する。防御が3層あるうち、真ん中の1層だけが抜けた状態だと理解するのが正確だ。
だからこそ、AWSが公式に挙げた回避策も「最小権限のIAM認証情報(例: ReadOnlyAccessロール)を使う」ことと、「接続が不安定な状態で起動してしまったなら、回復後に再起動する」ことの2点になっている。
引き金はAWS MCPサーバー起動時の「タイムアウト5秒のHTTP GET」
では、どういうときに読み込みが失敗するのか。core/metadata/read_only_operations_list.py を読むと、get_read_only_operations() の実体はネットワークアクセスである。
SERVICE_REFERENCE_URL = 'https://servicereference.us-east-1.amazonaws.com/'
DEFAULT_REQUEST_TIMEOUT = 5
class ServiceReferenceUrlsByService(dict):
def __init__(self):
super().__init__()
try:
response = requests.get(SERVICE_REFERENCE_URL, timeout=DEFAULT_REQUEST_TIMEOUT).json()
except Exception as e:
logger.error(f'Error retrieving the service reference document: {e}')
raise RuntimeError(f'Error retrieving the service reference document: {e}')
AWSのサービスリファレンス文書を、タイムアウト5秒で1回だけ取りに行く。実際にこのエンドポイントを叩くと、452サービス分・約69,898バイトのJSONが返る(本日の計測で応答0.22秒)。どの操作が読み取り専用かを判定するための索引データだ。
ここで効いてくるのが、MCPサーバーの起動タイミングという運用上の性質だ。MCPサーバーはClaude DesktopやCline、Cursorといったクライアントが起動時に子プロセスとして立ち上げる。つまり、次のような場面が現実的な引き金になりうる。
・ノートPCを開いた直後、Wi-FiやVPNの接続が確立する前にクライアントが起動した
・社内プロキシ経由の環境で、認証が通る前の一瞬にリクエストが飛んだ
・回線が混んでいて、応答に5秒以上かかった
・コンテナやCIで、エグレスが絞られていて外部に出られなかった
そして一度失敗すると、そのプロセスは再試行しない。MCPサーバーはクライアントを終了するまで生き続けるため、「朝の起動時に一瞬ネットワークが不調だった」だけで、その日の作業セッションの間ずっとポリシーが無効という状態が成立する。
起動後にもネットワークアクセスは走る(が、そちらは素通りしない)
やや細かいが、同じファイルにあるもう1つのネットワークアクセスと対比すると、この脆弱性の性質がはっきりする。サービスごとの詳細な操作一覧は、起動時ではなくそのサービスが初めて使われたときに遅延取得される。
def _cache_ready_only_operations_for_service(self, service: str):
try:
response = requests.get(self._service_reference_urls_by_service[service],
timeout=DEFAULT_REQUEST_TIMEOUT).json()
except Exception as e:
logger.error(f'Error retrieving the service reference document: {e}')
raise RuntimeError(f'Error retrieving the service reference document: {e}')
こちらも同じ5秒タイムアウトのHTTP GETで、失敗すれば RuntimeError を投げる。だがこの例外はリクエスト処理の途中で発生するため、握りつぶされずにエラーとして呼び出し元へ返る。結果として「判定できないので実行しない」という安全側の挙動になる。
同じファイル・同じ通信手段・同じ失敗の種類でありながら、起動時に起きたか、リクエスト中に起きたかで結果が正反対になっていた。CVE-2026-16584が突いたのは、この非対称性そのものだ。
攻撃者にできることと、できないこと
ここは事実と解釈をはっきり分けて書く。アドバイザリは攻撃シナリオについて何も述べていない。以下は本記事がCVSSベクタとコードから導いた読みである。
・攻撃者はこの初期化失敗を任意に引き起こせない。だからこそ AC:H(攻撃条件が複雑)であり、EPSSも極めて低い
・攻撃者に必要なのは、失敗が起きている間にエージェントへ拒否対象のコマンドを発行させることだけ。これはMCPサーバーに固有の新しい攻撃面ではなく、エージェントに実行権限を渡している以上つねに存在する通常の脅威モデルである
・したがって現実的なリスクは「狙って攻撃される」ことより、「防御を設定した気になっている期間が生じる」ことにある
自分の環境を確認する手順
冒頭に挙げたコマンドを、判断の順序に沿って整理し直す。重要なのは、バージョン該当と実際の発火を分けて確認することだ。
# ① バージョン確認:1.3.47未満なら「該当バージョン」
pip show awslabs.aws-api-mcp-server 2>/dev/null | grep -i version
uv tool list 2>/dev/null | grep -i aws-api-mcp
# ↑どちらにも出ない場合は uvx の使い捨て環境で動いている。
# MCPクライアントの設定に書かれた起動コマンドのバージョン指定が実効値になる
grep -rn "aws-api-mcp-server" ~/.claude.json ~/.cursor/mcp.json 2>/dev/null
# ② ポリシーを設定していたか(していた場合のみ実害が出うる)
test -f ~/.aws/aws-api-mcp/mcp-security-policy.json && \
python3 -m json.tool ~/.aws/aws-api-mcp/mcp-security-policy.json
# ③ 環境変数による制限を併用していたか
env | grep -E 'READ_OPERATIONS_ONLY|REQUIRE_MUTATION_CONSENT'
# ④ 実際に発火したか:サーバ自身のログファイルを検索する
grep "Failed to load read operations index" ~/.aws/aws-api-mcp/aws-api-mcp-server.log*
# ⑤ ポリシーが実際に読み込まれた件数を確認する(0件や未出力なら効いていない)
grep -E "Loaded [0-9]+ commands in (denylist|elicit list)" ~/.aws/aws-api-mcp/aws-api-mcp-server.log*
ログの出力先はクライアント依存ではなく、サーバ自身がコード内で決めている。server.py の冒頭で Path.home() / '.aws' / 'aws-api-mcp' に aws-api-mcp-server.log を作り、loguru のシンクを追加している(標準エラー出力にも同時に流れるため、MCPクライアント側のログにも現れる)。
log_dir = Path.home() / '.aws' / 'aws-api-mcp'
log_file = log_dir / 'aws-api-mcp-server.log'
logger.add(log_file, rotation='10 MB', retention='7 days')
上の
retention='7 days' が示すとおり、ログは7日で破棄され、10MBでローテートされる。つまり④は「行が見つかった場合」だけが結論を出せる——見つかれば、そのプロセスの寿命の間はdenyListが効いていなかったと確定できる。一方で行が無いことは「安全だった」証明にはならない。影響期間は315日あり、その大半はすでにログが残っていない。過去に遡って安全を確認する手段は存在しないので、バージョンと、denyListを設定していたかどうかを判断の基礎に置くこと。
パッチを当てても残る2つの「黙って効かない」パターン
コードを読んでいて見つかった、CVE-2026-16584とは別の注意点を2つ挙げる。いずれもアドバイザリの記載範囲外で、1.4.0時点でも同じ実装のままだ。denyListが効かない原因を調べるときは、こちらも疑う価値がある。
① ポリシーファイルのJSONが壊れていても、警告だけで起動する。 _load_policy() の例外処理は logger.error() を出したあと、そのまま処理を続ける。結果として拒否リストは空のままサーバーが動く。
② ポリシーファイルの形式がバージョンで変わっている。 0.2.13ではトップレベルの denyList を読んでいたが、現行版は policy キーの下を読む。
# 1.3.46 以降(現行)
policy = policy_data.get('policy', {})
if 'denyList' in policy:
self.denylist = set(policy['denyList'])
古い形式のまま書いたファイルは、エラーにもならず単に0件として読み込まれる。読み込めたときだけ Loaded N commands in denylist が出力されるので、起動ログにこの行が出ているか、件数が想定どおりかを確認するのが確実だ。
対策:1.3.47への更新と、その副作用
対策は明快で、1.3.47以上へ更新する。AWSは最新版への更新と、フォークや派生コードにも修正を取り込むことを推奨している。
# 更新
pip install -U "awslabs.aws-api-mcp-server>=1.3.47"
# uvx で起動している場合はバージョンを明示的に固定する
uvx awslabs.aws-api-mcp-server@latest
注意点として、MCPクライアントの設定ファイルで uvx [email protected] のようにバージョンを固定している場合、pip install -U を実行してもクライアントが起動するのは固定したバージョンのままになる。更新後は、MCPクライアント側の設定に書かれた起動コマンドも合わせて確認すること。コンテナイメージに焼き込んでいる場合も同様に、イメージの再ビルドが必要になる。
更新できるまでの緩和策として、AWSは次のいずれかを挙げている。
・最小権限のIAM認証情報を使う(例: ReadOnlyAccessロール)。IAMは主たるアクセス制御として、この不具合と無関係に機能し続ける
・接続が不安定な状態で起動してしまった場合は、回復後にサーバーを再起動する。索引の読み込みは起動時にしか行われないため、再起動が唯一の復旧手段になる
更新すると「起動しなくなる」ことがある
ここが実務上いちばん重要な副作用だ。修正版で何が変わったかというと、fail-open(黙って通す)をfail-closed(起動しない)に変えただけである。
# 1.3.47(修正版)— 起動処理
try:
READ_OPERATIONS_INDEX = get_read_only_operations()
except Exception as e:
logger.error(
'Failed to load read operations index required for security policy '
'enforcement; refusing to start: {}', e,
)
raise # ← 起動を中止する
リクエスト側にも、索引が無ければ拒否するガードが入った。加えて、環境変数だけを見ていた else 経路は削除されている(この2つの環境変数は元々 determine_policy_effect() の中でも評価されているため、機能は失われない)。
重要なのは、読み込み処理そのものには一切手が入っていないことだ。1.3.46→1.3.47の差分をパッケージ全体で取ると、変更されたのは server.py と __init__.py だけで、read_only_operations_list.py は1バイトも変わっていない。キャッシュもリトライもオフラインモードも追加されていない。
| 観点 | 1.3.46以前 | 1.3.47以降 |
|---|---|---|
| 索引の読み込みに失敗したとき | warningを出して起動を続行 | errorを出して起動を中止 |
| そのときのdenyList判定 | スキップされる | (そもそも起動しない) |
| リクエスト時に索引が無い場合 | 環境変数のみで判定 | 拒否する |
| 再試行・キャッシュ | なし | なし(変更なし) |
| オフライン環境での起動 | 起動する(ポリシーは無効) | 起動しない |
エグレス制限のあるコンテナ、CIランナー、閉域VPC、プロキシ必須の社内環境で動かしている場合、1.3.47以降は
servicereference.us-east-1.amazonaws.com へ5秒以内に到達できないとサーバーが起動しません。上げる前に、実際に動かす環境から到達性を確認してください(本記事冒頭のコマンド④)。これは不具合ではなく、意図された安全側の設計変更です。
1.4.0で示された終息予告と、MCPサーバー設計の教訓
パッチの翌日(JST 2026-07-23 16:02)に公開された1.4.0を1.3.47と比較すると、変更されたファイルは3つだけで、内容はセキュリティ修正ではなかった。サーバーの説明文と各ツール定義に、次の非推奨通知が埋め込まれている。
AWS API MCP serverは開発の終息(end of development)に入る。最新および新規のAWS APIへのアクセスを維持するため、AWS MCP Serverへの移行を推奨する。
SUPPRESS_DEPRECATION_WARNING で抑止できるが、既定ではサーバーの instructions とツール説明文の双方に載るため、エージェントの応答にも移行の案内が現れる設計になっている。セキュリティ修正のために1.3.47以上へ上げることは依然として必要だが、そのうえで移行先の検討が前提になった、というのが現在地だ。
なお、1.4.0でも read_only_operations_list.py は変更されておらず、前節で述べた「オフラインでは起動しない」という性質はそのまま維持されている。1.3.47と1.4.0の差分でパッケージ内の変更対象は3ファイル(server.py・core/common/config.py・__init__.py)のみで、いずれも非推奨通知の実装に関わるものだ。セキュリティ上の挙動は1.3.47と同一なので、この2つのどちらを選んでもCVE-2026-16584への対処としては等価である。
移行を検討する場合も、まず1.3.47以上へ上げて穴を塞いでから移行計画を立てる順序が現実的だろう。移行にはツール名やポリシーファイルの扱いの違いを確認する時間が要るが、脆弱性への対処は今日できる。
fail-open と fail-closed のどちらに倒すか
この事例が示しているのは、CWE-455(Non-exit on Failed Initialization)という分類名そのものだ。初期化に失敗したときプロセスを終わらせるかどうかが、そのまま防御の有無を決めてしまう。
logger.warning() と logger.error() + raise の差は、コードの見た目としては数行にすぎない。だが運用上の意味はまったく違う。前者は「気づかないまま無防備に動き続ける」、後者は「動かないので必ず気づく」。セキュリティ機構の初期化においては、うるさく壊れるほうが望ましいという設計判断がここで採用された。
・制御の失敗を可用性の問題として扱う:ポリシー判定に必要なデータが無いなら、動かさない
・外部依存を初期化の必須要件にするなら、失敗時の扱いを先に決める:今回の索引はネットワーク越しに取得するデータで、失敗する前提を織り込むべきだった
・警告ログは防御ではない:誰も読まないログに1行出すことは、制御が働いている状態と同じではない
興味深いのは、同じコードベースが別の場所ではきちんとfail-closedを選んでいることだ。前述の遅延取得は失敗を呼び出し元へ伝えるし、同意要求リスト(elicitList)の処理には次の分岐がある。
if api_call in self.elicit_list:
# クライアントが同意要求に対応していなければ、拒否として扱う
if not self.supports_elicitation:
return PolicyDecision.DENY
return PolicyDecision.ELICIT
「同意を求めたいが、求める手段が無い」という状況で、通すのではなく拒否を選んでいる。設計者はfail-closedの考え方を持っていた。それでも起動処理の1か所だけが逆に倒れていた——というのが、この事例のいちばん現実的な教訓かもしれない。方針として理解していることと、あらゆる失敗経路でそれが貫かれていることは別物だ。
自分でMCPサーバーを実装する場合、次の3点を確認するだけでも同種の穴は塞げる。
・起動時に読み込む外部データが、権限判定に使われていないか。使われているなら、読み込み失敗時に起動を中止するか、判定を拒否側に倒す
・「設定が読めなかった」ときのログレベルは適切か。warning で済ませている箇所は、実質的に無視される
・設定ファイルのパース失敗が、空の設定として扱われていないか。空の拒否リストは「何も拒否しない」と同義である
エージェントに実行権限を渡すMCPサーバーは、この判断を迫られる場面が多い。同種の設計上の問題は他のMCP実装でも起きており、MCP脆弱性!STDIOトランスポートの設計欠陥で20万台のサーバーがRCEの危険に——OX Securityが警告 では、トランスポート層の前提が攻撃面になった例を扱っている。運用中のサーバーが外部から狙われている実態については 公開 MCP サーバが標的に|AI 開発者を狙い撃つ走査を SANS ISC が観測 が、認証情報そのものが奪われる経路については Claude CodeのMCP通信ハイジャックでOAuthトークンが窃取される攻撃|検知・対応・防御の手順 が参考になる。
CVE-2026-16584は、AWS API MCP Serverが起動時の初期化に失敗したとき、セキュリティポリシーの判定を黙ってスキップしていた問題。素通りしたのは
mcp-security-policy.json のdenyList/elicitListで、環境変数による2つの制限とIAM権限は維持されていた。0.2.13(2025-09-10)から1.3.46まで約10か月半、この経路は残り続けた。対策は1.3.47以上への更新だが、修正はfail-openをfail-closedに変えたものであり、オフライン環境では起動しなくなる点を先に確認する必要がある。
参照ソース
・GHSA-29w2-fq35-v728 — AWS API MCP Server Security Policy Bypass via Startup Initialization Failure — 影響範囲・CVSS・回避策の一次情報
・awslabs/mcp PR #4315 — Security Policy Bypass via Startup Initialization Failure — 修正の意図と挙動変更の説明
・修正コミット ab1bbebc097d674c1cdd4bd75a8f313be18473bf — 本記事が引用した差分の出所
・AWS security bulletin 2026-063-aws — AWS公式の告知
・PyPI: awslabs.aws-api-mcp-server — バージョンごとの公開日時と配布物(1.3.46/1.3.47/1.4.0のsdistを取得して差分を確認)