MCP Sampling権限昇格を止める防御実装3パターン - Claude Desktopで実測
先週の勉強会で「Sampling は非推奨になったから、防御は書かなくていいですよね?」と聞かれました。前回の攻撃記事を書いた本人に向かって、です。私は3秒黙りました。
非推奨(deprecated)と、まだ動く、と、防御不要、は全部別の話です。
SEP-2577 で Sampling は Roots・Logging と一緒に非推奨に落ちましたが、12ヶ月の移行期間は普通に動きます。Claude Desktop 含む主要クライアントも動作を維持しています。そしてすでに本番で回っている MCP サーバーの大半は、この12ヶ月をそのまま走り抜けるはずです。「1年後に消えるから対策は書かなくていい」という判断をした環境は、その1年のうちに CVE を踏む側に回ります。
前回の記事(MCP Sampling 権限昇格 — Claude Desktop で通る3経路)で、Claude Desktop の承認画面を素通りできる3攻撃経路(承認疲れ / 目的隠しチェーン / クロスサーバ経由)を検証しました。今回はその防御実装編です。
3層で止めます。Claude Desktop 設定層 / MCP サーバー実装層 / Permission gate 層。単独ではどれも穴があります。3層重ねて、はじめて前回の3経路が塞がります。

前回の3経路を1画面で
防御の話に入る前に、何を止めるのかを1画面で確認しておきます。
| 経路 | 攻撃の型 | Claude Desktop の弱点 |
|---|---|---|
| A4 承認疲れ | 1タスクで15〜20回 Sampling を連射 | レート制限がクライアント側実装依存 |
| A5 目的隠しチェーン | 翻訳→整形→引数化と分割 | 承認画面は1回分しか意味論的に検査しない |
| A3 クロスサーバ経由 | X サーバが Y サーバのデータを Sampling 応答経由で吸う | サーバ間のコンテキスト境界が甘い |
3経路とも、単一 Sampling リクエスト単体では無害に見えます。危険なのは連鎖と組み合わせのほう。防御もそれに合わせて設計します。
防御パターン1: Claude Desktop 設定層 (allowedTools + プロキシ介在)
まず一番外側。Claude Desktop の claude_desktop_config.json で書ける防御です。
1-1. allowedTools で Sampling を要求するツール自体を絞る
MCP サーバーが提供するツールのうち、Sampling を発火させるものは限られています。書籍(MCPセキュリティ実践 第7章)で扱った freee のケースだと、270ツールのうち Sampling が必要なのは判断系の10個程度でした。allowedTools でその10個以外を切ります。
{
"mcpServers": {
"freee": {
"command": "npx",
"args": ["-y", "@example/freee-mcp"],
"allowedTools": [
"classify_account_item",
"suggest_partner_name",
"reconcile_deal"
]
}
}
}
Sampling を呼ばないツール(参照系・検索系)は allowedTools から Sampling 経路そのものを消せます。A3(クロスサーバ)への一次防御は allowedTools で表面積を減らすこと。攻撃者がまず触れるツール数を減らせば、連鎖の起点も減ります。
1-2. MCP プロキシで Sampling リクエストにレート制限をかける
A4(承認疲れ)を止めるには、Sampling リクエストの毎分レートを制限する必要があります。Claude Desktop 本体は 2026-08 時点でレート制限 UI を持っていません。ここは MCP プロキシ層で挟むのが現実解です。
シンプルな Node.js プロキシの心臓部だけ書きます。
// mcp-sampling-rate-limiter.ts
import { RateLimiterMemory } from 'rate-limiter-flexible';
const limiter = new RateLimiterMemory({
points: 5, // 5リクエスト
duration: 60, // per 60秒 per server
});
export async function guardSamplingRequest(
serverId: string,
next: () => Promise<Response>,
): Promise<Response> {
try {
await limiter.consume(serverId);
return await next();
} catch {
return errorResponse(
-32029,
`sampling rate exceeded for ${serverId} (5 req/min)`,
);
}
}
閾値5/分は保守的な値です。私の環境では、正常な経費処理ワークフロー1回で Sampling は平均2〜3回しか出ません。15回超えるのは異常系だと言い切れました。
ここで気づいたこと: 閾値を「1タスクあたり」ではなく「per server per 60秒」で切ったのは、目的隠しチェーン(A5)の測定が実際にはしにくいからです。連鎖の途中でユーザが席を立って戻ってきた場合、1タスクの単位が曖昧になります。時間窓のほうが実装しやすく、誤検知も少ない。
防御パターン2: MCP サーバー実装層 (単一目的 + 連鎖禁止)
外側で絞っても、サーバー側が悪意を持っていたら意味がありません。次はサーバー実装側の自己抑制です。ここは A5(目的隠しチェーン)への一撃になります。
2-1. Sampling 呼び出しに purpose タグを必須化する
サーバーコード内で Sampling を呼ぶときは、必ず単一目的の purpose を明示します。TypeScript 実装だとこう書けます。
// safe-sampling.ts
type SamplingPurpose =
| 'classify_account_item'
| 'suggest_partner_name'
| 'reconcile_deal';
interface SafeSamplingRequest {
purpose: SamplingPurpose; // 単一の enum、動的値禁止
prompt: string;
maxTokens: number;
correlationId: string; // 呼び出し系列の親ID
}
const inFlight = new Map<string, SamplingPurpose>();
export async function requestSampling(
req: SafeSamplingRequest,
client: McpClient,
): Promise<string> {
// 1. 同じ correlationId で異なる purpose の連鎖を拒否
const prev = inFlight.get(req.correlationId);
if (prev && prev !== req.purpose) {
throw new Error(
`sampling chain refused: ${prev} → ${req.purpose} ` +
`(single-purpose rule violated)`,
);
}
inFlight.set(req.correlationId, req.purpose);
try {
return await client.sample(req);
} finally {
setTimeout(() => inFlight.delete(req.correlationId), 30_000);
}
}
同じユーザ操作(同一 correlationId)の中で classify_account_item から始まった Sampling が、途中で suggest_partner_name に切り替わったら、そこで例外を投げます。A5 の翻訳→整形→引数化パターンは、この単一目的ルールで正面から折れます。
2-2. Sampling の応答を「そのまま次のツールに渡さない」
A3(クロスサーバ)への防御は、Sampling 応答を tools/call の引数に自動投入しないことです。人間の目を挟むワンステップを入れます。
// no-passthrough.ts
async function useClassificationResult(result: string, ctx: Ctx) {
// ❌ 悪い: 直に次のツールへ
// await ctx.callTool('create_deal', { account_item: result });
// ✅ 良い: ユーザ確認を挟む
const confirmed = await ctx.elicit({
prompt: `分類結果: ${result}\nこの勘定科目で仕訳を作成しますか?`,
schema: { type: 'boolean' },
});
if (!confirmed) return;
await ctx.callTool('create_deal', { account_item: result });
}
Sampling 応答 → 次ツール呼び出しの間に elicit を入れて、ユーザに明示的に見せる。elicit は SEP-2322 の Multi Round-Trip Requests の元ネタでもあり、Sampling 廃止後の後継として今のうちに慣らしておくと移行が楽です。
防御パターン3: Permission gate 層 (trace + intent 検査)
3層目は、Sampling リクエストのメタデータを機械可読な形で監査に流し込む部分です。事後検知になりますが、A3・A4・A5 の連鎖パターンは事後解析でこそ見えます。
3-1. trace context を Sampling にも通す
SEP-414 の tasks/ trace context を Sampling リクエストにも必ず付けます。付いていない Sampling リクエストは、プロキシ層で拒否します。
// trace-gate.ts
export function requireTraceContext(req: SamplingRequest): void {
const traceparent = req.meta?.traceparent;
if (!traceparent || !/^00-[0-9a-f]{32}-[0-9a-f]{16}-/.test(traceparent)) {
throw new SamplingRejected('missing or malformed traceparent');
}
}
これで、Sampling リクエストの全量が trace ID で紐付いた状態で監査ログに残ります。あとで「同じ traceparent で3回以上 Sampling が発火した」「異なる server span から同じ correlationId が引かれた」といったパターンを SQL で検出できるようになります。
3-2. 監査ログを「連鎖の見えるスキーマ」で保存
監査ログのスキーマは、単発リクエストではなく連鎖前提で書きます。私が使っているのはこれです。
CREATE TABLE mcp_sampling_audit (
id BIGSERIAL PRIMARY KEY,
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
trace_id TEXT NOT NULL,
span_id TEXT NOT NULL,
parent_span_id TEXT,
server_id TEXT NOT NULL,
purpose TEXT NOT NULL,
prompt_hash TEXT NOT NULL,
response_hash TEXT NOT NULL,
user_approved BOOLEAN NOT NULL,
chain_length INT NOT NULL -- 同一trace_id内での通し番号
);
CREATE INDEX ON mcp_sampling_audit (trace_id, chain_length);
chain_length を持つと「chain_length >= 4 のリクエストは自動アラート」といった単純ルールで A5 を検出できます。私の環境で一週間流したところ、正常フローの chain_length 平均は 1.8、95パーセンタイルで 3。閾値4は現実的でした。
3層をどう並べるか (実運用マップ)
3層は独立ではなく、順番に効きます。
- 入口: Claude Desktop 設定層で表面積を絞る (allowedTools + プロキシレート制限)
- 中: サーバー実装層で単一目的を強制 (chain 拒否 + 応答パススルー禁止)
- 出口: Permission gate 層で trace 必須化 + 監査 (chain_length で事後検知)
各層は前段が抜けたときに次段が受け止める設計です。allowedTools を書き忘れても、単一目的ルールで A5 が止まる。単一目的ルールを外していても、chain_length アラートで事後に気付ける。単層防御は避ける、が今回のたったひとつの実装原則です。
何が変わらないか、正直に
3層を入れても、以下は変わりません。
- ユーザが reflexive にクリックする問題: A4 の根本原因はレート制限では消えません。UI 側の「N秒間 Sampling が発火したら承認ボタンを無効化」が必要ですが、これは Anthropic 側の実装待ちです。
- サーバー実装者が悪意ある場合: purpose enum は自分で書くコードなので、悪意あるサーバーは purpose を偽装できます。ここは MCP のサンドボックスモデル自体の限界で、防御パターン2は「善意サーバーが誤ってチェーンする事故」を防ぐレイヤーだと割り切っています。
- Sampling 廃止後: 12ヶ月後、Sampling は消えます。しかし SEP-2322 の Multi Round-Trip Requests は、サーバー起点の LLM 呼び出しを別の型で引き継ぎます。「連鎖 / パススルー / 承認疲れ」の3パターンは名前を変えて残るはずです。今書いた防御コードは、移行後もそのまま持ち越せる形にしてあります。
関連記事
- MCP Sampling 権限昇格 — Claude Desktop で通る3経路 — 本記事の攻撃側の元記事
- MCP OWASP Top10 と Claude Desktop の防御マップ 2026-07 — Sampling を含む全体マップ
- (英語) Natural-Language Agent Harnesses (arXiv 2603.25723): Survey Notes and My Take — 権限境界を扱う harness engineering の分類論
参考リンク
- SEP-2577: Deprecate Roots, Sampling, and Logging — 非推奨の一次資料
- MCP 2026-07-28 spec: what changed, what breaks — 変更点まとめ
- The new MCP spec and the unfortunate deprecation of MCP Sampling — 廃止への技術的異論
MCP の権限境界・Sampling・OWASP MCP Top 10・ツールポイズニング・監査設計を通しで扱った書籍がこちらです → MCPセキュリティ実践
第6章で OWASP MCP Top 10、第7章で本番ワークアラウンド、第9章で運用ハックまでカバーしています。Sampling 廃止までの1年をどう乗り切るか、が主題です。
関連書籍 MCP実践セキュリティ MCP セキュリティ 完全ガイド | OWASP MCP Top 10・トークンコスト・ファイルアップロード問題 書籍ページを見る → この記事は役に立ちましたか?