xinference(xorbitsai/inference)に、CVSS 10.0 の任意コード実行脆弱性 CVE-2026-61539 が 2026-08-21 に公表されました。原因は、Llama3 系モデルが返した「ツール呼び出しの出力」を、Python の素の eval() に渡していたことです。モデルの出力は攻撃者のプロンプトで誘導できるため、チャット API に投げた文章が、そのままサーバー上のコード実行になり得ます。今回の Xinference 脆弱性は、いわゆる eval インジェクション(CWE-95)に分類されます。本記事では、PyPI で実際に配布されているホイールを取得して脆弱なコードと修正後のコードを突き合わせ、影響範囲・確認手順・対策を整理します。攻撃に使える再現手順は載せません。

CVE-2026-61539の時系列図。2026年4月12日に脆弱な2.5.0が公開され、4月25日に修正版2.7.0が公開、5月23日に2.9.0でOCR経路も修正、8月21日にCVEが公表される流れを示す。
CVE-2026-61539 の時系列。修正版 2.7.0 の公開は 2026-04-25 で、CVE 公表(2026-08-21)の約4か月前だった(PyPI のアップロード日時と GitHub Advisory を実測)。

30秒でわかる|この記事のポイント

何が起きた:Xinference が Llama3 のツール呼び出し出力を eval(text, {}, {}) に渡していた。CVE-2026-61539 / CVSS 10.0 / CWE-95(Eval Injection)
なぜ深刻:モデル出力はプロンプトで誘導できる。アドバイザリの検証構成では認証が無効で、未認証の攻撃者が /v1/chat/completions から到達できた
影響範囲2.7.0 未満。本記事では 2.4.0 / 2.5.0 の配布ホイールに該当コードが実在することを確認した
対処2.7.0 以上へ更新。ただし同種のパターンが別経路(DeepSeek-OCR)に 2.9.0 まで残っていたことも実測した
時間差:修正の公開は 2026-04-25、CVE 公表は 2026-08-21。約4か月のずれがある

なお、AIツールに外部入力が届く経路が丸ごと攻撃面になる構造を体系的に押さえたい場合は、サプライチェーン攻撃とは|手口・防御ツール比較・npm 12の新機構まで実践解説 を合わせて読んでください。本記事はそのうち「LLM の出力を信頼して実行してしまった」一点に絞った解説です。

xinference とは:1行の変更でモデルを差し替える OSS 推論サーバー

Xinference(リポジトリ名は xorbitsai/inference)は、ローカルや自社サーバーで LLM・埋め込みモデル・音声モデル・画像モデルをまとめてホストし、OpenAI 互換 API として提供するオープンソースの LLM 推論サーバーです。GitHub 実測で ★9,517・fork 862、直近の push は 2026-08-25 と現役で開発が続いています。ライセンスは Apache-2.0 です。

公式リポジトリの説明文は「Swap GPT for any LLM by changing a single line of code」——つまり OpenAI の SDK を向き先だけ変えてそのまま使えることを売りにしています。日本語圏では Dify との組み合わせで使われる例が多く、「Dify から Xinference を利用するための環境構築手順」といった記事が検索上位に並びます。裏を返すと、Dify などのアプリ層の裏側で動く推論サーバーとして導入されているケースが多いということです。

今回の脆弱性で重要なのは、Xinference が OpenAI 互換の /v1/chat/completions を持つという点です。攻撃の入口は特殊な管理APIではなく、アプリが普通に叩いているチャットのエンドポイントそのものでした。

Xinference には「ツール呼び出し(function calling)」の機能があり、モデルが「この関数を、この引数で呼びたい」という構造をテキストで返してくると、サーバー側がそれをパースして関数名と引数に分解します。今回問題になったのは、この分解処理です。

読者の3つの問いへの答え
何ができる:多種のオープンモデルを OpenAI 互換 API で一括ホストできる。② 何を解決する:モデルごとにバラバラな推論の作法を1つのエンドポイントに統一する。③ 何を代替できる:vLLM・Ollama・LM Studio などの単体ランタイムを、複数モデルを束ねる層として置き換えられる。

CVE-2026-61539 の時系列——修正は公表の4か月前に出ていた

日付は PyPI のアップロード日時と GitHub Advisory / NVD の実測値です。

日付 出来事
2026-03-29 2.4.0 公開。該当コードあり(実測)
2026-04-12 2.5.0 公開。該当コードあり(実測)
2026-04-25 2.7.0 公開。CVE-2026-61539 の修正が入る(PR #4786 / commit 1b3d220
2026-05-09 2.8.0 公開。OCR 経路の素の eval() はまだ残る(実測)
2026-05-23 2.9.0 公開。OCR 経路も ast.literal_eval に置換(実測)
2026-08-17 3.2.1 公開(本記事執筆時点の最新)
2026-08-21 CVE-2026-61539 / GHSA-x2rj-828p-hx9m 公表。CVSS 10.0
2026-08-26 本記事の検証時点(★9,517 を実測)

ここで目を引くのは、修正版 2.7.0 の公開(2026-04-25)と CVE 公表(2026-08-21)の間に約4か月あることです。調整開示の流れとしては珍しくありませんが、利用者側から見ると意味が二つに割れます。

通常どおり更新を続けていた環境は、脆弱性の存在を知らないまま、いつの間にか安全になっていた
バージョンを固定していた環境は、公開情報が何もない状態で4か月間、露出し続けていた

バージョン固定の落とし穴
再現性のために xinference==2.5.0 のようにピン留めする運用は一般的ですが、この4か月のように「修正は出ているが CVE はまだ無い」期間には、脆弱性データベースを監視していても検知できません。ピン留めした依存は、CVE の有無とは別に定期的な追随が要ります。

なお 2.6.x は存在しません。PyPI のリリース一覧でも GitHub のタグでも 2.5.0 の次は 2.7.0 で、番号が飛んでいます。「2.6 系なら大丈夫か」という判断は成立しないので注意してください。

何が起きたのか:モデルの出力がそのまま eval() に渡る

NVD に登録された説明文は、経路を明示しています。要点は「Xinference が、攻撃者に影響される Llama3 のツール呼び出し出力を eval() に渡していた」ことと、「未認証のリモート攻撃者が、細工したプロンプトを通じてその出力に影響を与え、Xinference サーバープロセスの権限でコマンドを実行できた」ことです。

実際に PyPI から 2.5.0 のホイールを取得して展開したところ、該当コードは2か所にありました。

xinference/model/llm/utils.py(753行目)
xinference/model/llm/tool_parsers/llama3_tool_parser.py(46行目)

どちらも同じ形をしています。

# xinference 2.5.0 / model/llm/utils.py(脆弱版・抜粋)
@classmethod
def _eval_llama3_chat_arguments(cls, c) -> List[Tuple]:
    text = c["choices"][0]["text"]
    try:
        data = eval(text, {}, {})
        return [(None, data["name"], data["parameters"])]
    except Exception:
        return [(text, None, None)]

c["choices"][0]["text"]モデルが生成したテキストそのものです。それを eval() に渡しています。第2・第3引数に空の辞書を渡してグローバル・ローカル名前空間を空にしていますが、これは防御になりません。Python の eval() は空の名前空間でも組み込み関数へ到達する経路が残るため、名前空間を空にしただけでは任意コード実行を防げないことが知られています。

さらに、llama3_tool_parser.py 側の docstring は当時こう書かれていました。

# xinference 2.5.0 / tool_parsers/llama3_tool_parser.py(抜粋)
"""
Parses the model output using eval() to extract tool call information.
This method expects the output to be a valid Python dictionary format.
"""
つまり eval() の使用は書き忘れや事故ではなく、「モデルは Python の辞書リテラルを返す」という前提に立った意図的な実装でした。前提が崩れるのは、モデルの出力を攻撃者が誘導できるときです。

攻撃の流れを構造だけで示すと、次のようになります。具体的なペイロードは載せません。

flowchart TD A["攻撃者
細工したプロンプトを送信"] --> B["/v1/chat/completions
(検証構成では未認証で到達可能)"] B --> C["LLM がツール呼び出し形式の
テキストを生成"] C --> D{"サーバー側で後処理"} D --> E["eval(text, {}, {})
モデル出力を評価"] E --> F["Xinference プロセスの権限で
任意コードが実行される"]

この形は、いわゆるプロンプトインジェクション RCE——プロンプトで誘導した出力が、そのままコード実行につながる連鎖です。ここでの本質は、信頼境界の置き場所を間違えたことです。多くの実装は「ユーザー入力は信頼しない」を守りますが、モデルの出力は自分の側の生成物なので信頼してよいと暗黙に扱いがちです。実際にはモデルの出力はユーザー入力の関数であり、ユーザー入力と同じだけ信頼できません

CVSS ベクタは AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H で、ネットワーク経由・低複雑度・権限不要・ユーザー操作不要、しかも Scope:Changed(影響が脆弱なコンポーネントの外に及ぶ)です。10.0 という最高値は、この「条件が何も要らない」点から来ています。

修正の中身:eval から ast.literal_eval へ

2.7.0 のホイールを同じように展開して該当箇所を見ると、実装が置き換わっています。

# xinference 2.7.0 / model/llm/utils.py(修正版・抜粋)
try:
    # Try JSON first (most common LLM output format)
    data = json.loads(text)
except (json.JSONDecodeError, TypeError):
    try:
        # Fall back to ast.literal_eval for Python literal formats
        # (e.g., True/False/None instead of true/false/null).
        # Unlike eval(), ast.literal_eval() only allows literal
        # structures and cannot execute arbitrary code.
        data = ast.literal_eval(text)
    except (ValueError, SyntaxError):
        return [(text, None, None)]

修正は二段構えです。まず json.loads() を試し、失敗した場合だけ ast.literal_eval() にフォールバックします。ast.literal_eval は文字列・数値・タプル・リスト・辞書・集合・真偽値・None というリテラル構造しか受け付けず、関数呼び出しを含む式を評価しません。コード中のコメントも「eval() と違って任意コードを実行できない」と明記しています。

この修正は素直で正しい対処です。ポイントは、「入力を検証する」ではなく「評価器そのものを、実行能力のないものに差し替えた」ことです。危険な関数に渡す前にフィルタを足す方向は、抜け道探しとのいたちごっこになりがちですが、literal_eval への置換は能力そのものを落とすため、原理的に強い対処です。

観点 2.5.0(脆弱) 2.7.0(修正後)
一次パーサ eval(text, {}, {}) json.loads(text)
フォールバック なし ast.literal_eval(text)
関数呼び出しの評価 実行される 実行されない(ValueError
想定していた入力 Python 辞書リテラル JSON、次点で Python リテラル
失敗時の挙動 例外を握りつぶして生テキストを返す 例外を握りつぶして生テキストを返す

影響範囲と対策:自分の xinference を確認する手順

Xinferenceの脆弱性確認手順のフロー図。バージョン確認、実体パス取得、grepで判定、2.7.0以上へ更新の4ステップ。
確認は4手で済む。バージョン番号だけで判断せず、実際に入っているファイルを見るのが要点。

まずバージョンを確認します。

# インストール済みバージョンを確認
pip show xinference | grep -i version
python -c "import xinference; print(xinference.__version__)"

ただし、バージョン番号だけで判定しないほうが確実です。派生パッケージ・ベンダリング・古いイメージの再利用など、番号と中身がずれる状況は珍しくありません。実際にインストールされているファイルを直接見ます。

# xinference の実体ディレクトリを特定して、素の eval( が残っていないか確認する
XI=$(python -c "import xinference, os; print(os.path.dirname(xinference.__file__))")

# 該当2ファイルに素の eval( があれば脆弱版の可能性が高い
grep -n "eval(text" "$XI/model/llm/utils.py" 2>/dev/null
grep -n "eval(model_output" "$XI/model/llm/tool_parsers/llama3_tool_parser.py" 2>/dev/null

# 修正版なら literal_eval 側にヒットする
grep -rn "ast.literal_eval" "$XI/model/llm/" 2>/dev/null | head

判定は次のとおりです。

eval(text / eval(model_outputヒットする → 修正前。2.7.0 以上へ更新する
ast.literal_evalヒットする → 当該経路は修正済み
・どちらも出ない → パスが違う可能性がある。find "$XI" -name "llama3_tool_parser.py" で実体を探し直す

この確認方法は、本記事で 2.5.0 / 2.7.0 / 3.2.1 の配布ホイールを展開して実際に動かして結果が分かれることを確認しています。

Xinferenceの各バージョンにおける素のeval()行数の棒グラフ。2.5.0が7行、2.7.0が4行、3.2.1が3行。

PyPI 配布ホイールを展開し、コメント行を除いて計数。3.2.1 に残る3行はいずれも thirdparty 配下。

</figure>

参考までに、各版で「コメント行を除いた素の eval( の出現行数」を数えると次のようになりました。

バージョン 素の eval( 行数 ast.literal_eval を使うファイル数 備考
2.5.0 7 0 Llama3 経路・OCR 経路の両方に該当あり
2.7.0 4 2 CVE の経路は修正。OCR 経路は残存
3.2.1 3 2 残りは thirdparty/ のベンダリングコードのみ
この数値の読み方(重要)
「素の eval( がゼロにならない」ことを不安に思う必要はありません。3.2.1 で残っている3行は、いずれも xinference/thirdparty/ 配下に取り込まれた外部プロジェクト(deepseek_vl2 の serve アプリ、megatts3 の設定パーサ)のコードで、Llama3 のツール呼び出し経路とは別物です。到達可能性は構成次第であり、本記事では検証していません(未検証)。

運用面では、バージョン更新に加えて次を確認してください。CVE そのものへの対処ではなく、同種の問題が起きたときに被害を狭めるための前提です。

推論サーバーを外部公開しない。Xinference のようなバックエンドは、アプリ層の背後に置き内部ネットワークに限定する
認証を有効にする。アドバイザリの検証構成では認証が無効だった。既定のまま運用しない
プロセス権限を落とす。root で動かさない。コンテナなら非特権ユーザーで実行する
ツール呼び出しを使わないなら無効にする。使っていない機能の攻撃面は落とす

2.7.0 で終わりではなかった——OCR 経路に残った同じパターン

配布物を版ごとに比較していて見つかったのが、同じ「モデル出力を素の eval() に渡す」パターンが、別のファイルにも存在していたことです。

xinference/model/image/ocr/deepseek_ocr.py には、2.5.0 の時点で次の2か所がありました。

・231行目:cor_list = eval(ref_text[2])
・399行目:coords = eval(f"[{coords_str}]")

ref_text は OCR モデルが返した参照テキストで、coords_str も同様にモデル出力に由来する座標文字列です。構造としては CVE-2026-61539 とまったく同じ——モデルの出力を素の eval() に渡しています。

GitHub のタグを版ごとに取得して該当ファイルを数えたところ、次のように推移していました。

バージョン 素の eval( 行数 literal_eval 行数
2.7.0(CVE 修正版) 2 0
2.8.0 2 0
2.9.0 0 6
2.10.0 以降 0 6

つまり CVE-2026-61539 が修正された 2.7.0 の時点では、OCR 側の同種パターンは手つかずで残っており、2.9.0(2026-05-23)でようやく ast.literal_eval に置き換わりました。3.2.1 の該当ファイルには「座標文字列はモデル出力に由来するので eval ではなく ast.literal_eval を使う」という趣旨のコメントが入っています。

1つの不備を直したときに、同じ設計上の勘違いが他のどこに現れているかを横展開して探せたか——ここが分かれ目でした。Xinference の場合、CVE の経路は 2.7.0 で塞がれた一方、同型の別経路は2リリース分(約1か月)遅れて修正されています。

これは当サイトで以前扱った Flowise 脆弱性CVE-2026-40933|CustomMCPで再燃したRCE、CVSS9.9の内実 と同じ形です。Flowise では CustomMCP ノードの JavaScript 評価を塞いだ半年後に、同じ機能の別の入力経路(stdio 引数のサニタイズ)で再燃しました。「1機能に1つ穴が見つかったら、その機能の入力経路を全部数える」という横展開が、どちらの事例でも遅れています。

なお、OCR 側の2か所には CVE が割り当てられていません。到達可能性(外部からどこまで誘導できるか)を本記事では検証していないため、「CVE 相当の危険がある」と断定はしません。確認できているのは、同じコードパターンが存在し、後から literal_eval に置き換えられたという事実だけです。

LLM 出力を信頼するとはどういうことか

この脆弱性が示しているのは、Xinference 固有の実装ミスというより、LLM をシステムに組み込むときの信頼境界の引き方です。

従来の Web アプリでは、信頼境界は「ユーザー入力 ↔ サーバー」の一本でした。LLM が入ると、境界が一段増えます。

flowchart LR U["ユーザー入力
(信頼しない)"] --> M["LLM"] M --> O["モデル出力"] O -.->|"ありがちな誤解
自分の生成物だから信頼できる"| T["そのまま実行・評価"] O ==>|"正しい扱い
ユーザー入力の関数=信頼しない"| V["リテラルとして解析
能力を落として受ける"]

Xinference の実装は、この図の点線側に立っていました。docstring が「モデル出力は有効な Python 辞書のはず」と書いていたのは、モデルを信頼できる部品として扱っていたことの表れです。

同じ構造の問題は、当サイトでも繰り返し扱っています。GitHubコメントがAIエージェントを乗っ取る:「Comment and Control」攻撃の仕組みと防御策 は、外部から書き込めるテキストがエージェントの指示として解釈される例で、向きは違いますが「テキストの出所を信頼してしまう」点は共通です。

他の推論サーバーはどう扱っているか(vLLM を実測)

「モデルが返すツール呼び出しをどうパースするか」は、どの推論サーバーも解かねばならない共通の課題です。Xinference の選択が業界の標準だったのかを確かめるため、代表的な OSS 推論サーバーである vLLM のツールパーサを実測しました。

XinferenceとvLLMのツール呼び出しパース方針の比較図。Xinference 2.5.0はeval()を使い、vLLMは50ファイルすべてでjson系パースを使う。
同じ課題に対する2つの実装方針。vLLM 側は main ブランチを tree API で全走査して計測した。

GitHub の tree API(?recursive=1)でリポジトリ全体を走査し、テストコードを除いた本体側のツールパーサ 50ファイルを取得して、コメント行を除く素の eval( の出現行数を数えた結果が次です。

実装 対象ファイル数 素の eval( 行数 JSON 系パースの出現数
vLLM(main・2026-08-26 取得) 50 0 84
Xinference 2.5.0(該当2ファイル) 2 2 0
Xinference 2.7.0(該当2ファイル) 2 0 2
vLLM は50個のツールパーサすべてで素の eval() を使っておらず、一貫して JSON としてパースしています。つまり Xinference の eval() は業界の慣行ではなく、この実装固有の選択でした。

これは vLLM が優れているという話ではなく、同じ課題に対して「実行能力のないパーサで受ける」という選択肢が最初から存在していたことの確認です。Xinference の 2.7.0 の修正が json.loads を一次パーサに据えたのは、結果的に vLLM と同じ設計に寄せたことになります。

なお、この計測はファイル単位の静的な文字列カウントであり、eval 相当の処理が別名や間接呼び出しで行われている可能性までは検証していません(未検証)。

実装者向けに、この事例から取り出せる原則を整理します。

モデル出力に対して eval / exec / pickle.loads / シェル実行を直接適用しない。使うなら json.loadsast.literal_eval のように能力の低いパーサを選ぶ
「名前空間を空にすれば安全」は成り立たないeval(text, {}, {}) は防御ではない
例外の握りつぶしに注意する。今回の実装は except Exception で失敗を吸収しており、攻撃の試行が痕跡を残さない構造でもあった
同じパターンをコードベース全体で数える。1か所直したら grep で横展開する

手元で確認したこと(検証環境)
本記事の数値は、macOS(Apple Silicon)上で PyPI から xinference の 2.5.0 / 2.7.0 / 3.2.1 のホイールを取得・展開し、該当ファイルを直接読んで確認したものです。バージョン間の eval( 行数は、コメント行を除いて数えています。OCR 経路の版差は GitHub のタグ指定で該当ファイルを取得して比較しました。攻撃の再現は行っていません。

まとめ

CVE-2026-61539 の要点

・Xinference が Llama3 のツール呼び出し出力を eval() に渡していた。CVSS 10.0 / CWE-95
2.7.0 未満が対象。2.7.0 以上へ更新すれば当該経路は塞がれる
・修正は json.loadsast.literal_eval の二段構え。能力を落としたのが正しい対処
・修正の公開(2026-04-25)と CVE 公表(2026-08-21)に約4か月のずれ。バージョン固定環境はこの間、公開情報なしで露出していた
同型のパターンが OCR 経路に 2.9.0 まで残っていた(実測)。余裕があれば 2.9.0 以上を選ぶ
・確認はバージョン番号でなく、インストール済みファイルへの grep で行うのが確実

Xinference は Dify などの背後で使われることが多く、自分が直接インストールした覚えがなくても動いていることがあります。まずは pip show xinference が何か返すかどうかから確認してみてください。

参照ソース

GHSA-x2rj-828p-hx9m(xorbitsai/inference 公式セキュリティアドバイザリ) — 影響範囲・修正バージョンの一次情報
NVD - CVE-2026-61539 — CVSS ベクタ・CWE・該当ファイルを含む説明文
xorbitsai/inference PR #4786 — 修正の実装(commit 1b3d220
xorbitsai/inference v2.7.0 リリース — 修正が入ったリリース
xinference — PyPI — 各版の公開日時とホイール(本記事の版差実測に使用)