2026年3月24日のLiteLLMサプライチェーン攻撃は、この投稿のきっかけになったものの、私が書いた理由がそれだけだったわけではありません。
バックドア付きの2つのバージョン(1.82.7と1.82.8)が、盗まれた認証情報を使ってPyPIに公開されました。マルウェアはSSHキー、クラウド認証情報、K8sのシークレットを盗みました。DSPy、MLflow、CrewAI、OpenHandsはいずれも、危険なパッケージを取り込みました。見逃していた場合は、Snykによる完全な内訳を読んでみる価値があります。
私は1年以上、さまざまなプロジェクトでLiteLLMを使ってきました。事件の後、数日かけて自分のスタックを監査し、代替案も検討しました。そこで分かったのは、単なるセキュリティ問題ではありませんでした。規模が大きくなるほど増幅し合う一連の問題のパターンだったのです。
もし今、LLMゲートウェイを評価しているのであれば、Bifrostは候補に入れておく価値があります。これは、これから述べるほとんどの問題を回避する、GoベースのオープンソースLLMゲートウェイです。
TL;DR
- サプライチェーン攻撃は、コンパイル済みバイナリには存在しない、Python固有の持続化メカニズムを悪用していました。
- LiteLLMはリクエストあたり約8msのレイテンシを追加します。規模が大きいと、これは無視できないほど重要になります。
- 推移的(トランジティブ)依存関係のツリーが巨大です。すべての
pip installは信頼判断です。 - 複数プロバイダの設定は、すぐに複雑になります。
- Pythonベースのプロキシは、開発環境よりも本番の負荷下で挙動が異なります。
1. サプライチェーン攻撃は単なる認証情報の漏えいではなかった
はっきり言います。認証情報の窃取は、どんなプロジェクトでも起こり得ます。攻撃者(TeamPCP)は、TrivyのGitHub Actionを侵害し、それをLiteLLMのCI/CDがピン留めなしで取り込んでいました。これにより、攻撃者はLiteLLMのPYPI_PUBLISHトークンを手に入れました。そこから、バックドア付きの2つのバージョンを公開しました。
しかし、注意すべきなのは持続化メカニズムです。
マルウェアは.pthファイルを配置しました。Pythonでは、site-packages内の.pthファイルが、すべてのインタプリタ起動時に任意のコードを実行します。これはMITRE ATT&CK T1546.018(Event Triggered Execution)として分類されます。クリーンなLiteLLMにロールバックしても、.pthファイルは残っていました。パッケージのアップグレードを生き延びたのです。そして、Pythonが起動するたびに、静かに(見えない形で)毎回実行されました。
攻撃者はさらに、73件の侵害されたGitHubアカウントを使って「開示(disclosure)」のイシューにスパムを送り、その後、盗んだメンテナの認証情報でイシューをクローズしました。コミュニティが、この侵害について知ることは、積極的に抑え込まれていました。
これは机上のリスクではありませんでした。LiteLLMには日次で3.4M+のダウンロードがあります。およそ3時間の間、すべてのpip install litellmで、AWS、GCP、Azureの認証情報、SSHキー、K8sのシークレットを流出(exfiltrate)するマルウェアが取得されました。
構造的な教訓はこうです。.pthによる持続化は、Python固有のベクタです。コンパイル済みのGoバイナリにはsite-packagesディレクトリもなく、.pthファイルもなく、インタプリタ起動時のフックもありません。攻撃対象領域(attack surface)がそもそも存在しないのです。
2. スケール時のPythonオーバーヘッド
LiteLLMはリクエストあたり約8msのレイテンシを追加します。プロトタイプや低トラフィックの社内ツールなら、それで問題ないでしょう。1回のAPI呼び出しで8msを気にする人はいません。
毎秒1,000リクエストの場合、あなたのフリート全体において、1秒あたりの累積の追加レイテンシは8秒になります。5,000 RPSなら、1秒あたりのオーバーヘッドは40秒です。レイテンシに敏感なパイプライン(リアルタイムのエージェント、ストリーミングチャット、バッチ評価)では、これは現実のコストに直結します。テールレイテンシが長くなる、タイムアウトが増える、リトライが増える—そうした形で複合的に効いてくるのです。
比較すると、Bifrostはリクエストあたり11µsのレイテンシオーバーヘッドを追加します。おおよそ700倍の差です。5,000 RPSでは、総オーバーヘッドは0.055秒/秒になります。
これは、Pythonという言語を下げる話ではありません。あなたのシステムが行うすべてのLLM呼び出しのクリティカルパスに、インタプリタ実行環境を置いたときに何が起きるか、という事実の話です。
3. 推移的依存関係による肥大化
pip install litellmを実行して、実際に環境に何が入るのか確認してください。依存関係のツリーは深いです。このツリー内のすべてのパッケージは、暗黙の信頼判断になります。推移的依存のそれぞれのメンテナには、あなたのインフラ上で実行されるコードに対する書き込み権限を持つ可能性があります。
2026年3月の攻撃は、これが机上の話ではないことを証明しました。侵害はLiteLLM自身のコードから始まったわけではありません。CI/CDパイプライン内の推移的依存(ピン留めされていないGitHub Action)から始まりました。爆発範囲が広がったのは、Pythonのパッケージングエコシステムが、コンパイル済みバイナリが持つような種類の隔離(isolation)を提供していないからです。
Bifrostは、単一のコンパイル済みGoバイナリとして提供されます。npxで実行することも、Dockerで実行することもできます。site-packagesディレクトリはありません。監査すべき推移的なpip依存はありません。事件後に調べるべき.pthファイルもありません。
# Bifrost: one binary, one trust decision
npx -y @maximhq/bifrost
# or
docker pull maximhq/bifrost
これは、特に規制のある環境で重要になります。セキュリティチームがLLMゲートウェイのすべての依存関係を監査する必要があるなら、「それは1つのバイナリです」という答えと、「推移的なPythonパッケージが47個あります」という答えでは、会話の内容がまったく変わります。
4. 設定の複雑さ
LiteLLMは、プロバイダのルーティングにYAMLベースの設定を使用します。1つか2つのプロバイダだけのシンプルな構成なら、それで機能します。しかし、フォールバック、ロードバランシング、モデルごとのルーティングまで含めて複数プロバイダを管理し始めると、設定ファイルは急速に膨らみます。
複数プロバイダのLiteLLM設定がどのように見えるか、簡略化した例を示します:
model_list:
- model_name: gpt-4
litellm_params:
model: openai/gpt-4
api_key: os.environ/OPENAI_API_KEY
- model_name: gpt-4
litellm_params:
model: azure/gpt-4-deployment
api_key: os.environ/AZURE_API_KEY
api_base: os.environ/AZURE_API_BASE
router_settings:
routing_strategy: least-busy
num_retries: 3
fallbacks:
- gpt-4: [claude-3-opus]
これを10のプロバイダと30のモデルにまで掛け合わせてください。カスタムヘッダ、プロバイダごとのタイムアウト、条件付きルーティングも追加します。そうなると、保守負担になります。
Bifrostは別のアプローチを取ります。プロバイダーの視覚的な設定のためのweb UIを備えており、ゼロコンフィグのデプロイに対応しています。プロバイダー鍵のための環境変数を設定すると、ゲートウェイが利用可能なモデルを自動的に検出します。
私は、YAMLの設定が本質的に悪いと言っているわけではありません。ただし、設定レイヤーがLLMインフラの中で最も脆い部分になっているなら、それは注意を払うべき設計上のシグナルです。
5. 「動くのは開発だけ」問題
これはもう少し見えにくいのですが、何度も繰り返し見てきました。Pythonベースのプロキシで、開発ではうまく動いていても、本番の同時実行環境では挙動が大きく変わります。
PythonのGIL(Global Interpreter Lock)により、プロキシ層のCPUバウンドな処理はスレッド間で直列化されます。LiteLLMはこれをasyncパターンで回避しており、その実装は適度な負荷では評価できるほど堅実です。しかし、重い同時負荷の下では実行時の特性が変わります。ガベージコレクションの停止、スレッドのスケジューリングに伴うオーバーヘッド、メモリ断片化などは、同時接続が1,000を超える場合と10の場合で挙動がまったく異なります。
Goはそのために設計されています。ゴルーチンは軽量です(各スタックは数KB)、スケジューラはプリエンプティブで、GILに相当するものがありません。Bifrostは、言語ランタイムが同時実行をネイティブに扱えるからこそ、巧妙な最適化ではなく、11µsのオーバーヘッドで5,000RPSを処理します。
LLMゲートウェイが開発規模のトラフィックだけを扱うのであれば、これは問題になりません。本番のユーザーを抱えるアプリケーションでプロダクショントラフィックをルーティングするなら、ランタイムの選択は非常に重要です。
クイック比較
| LiteLLM | Bifrost | |
|---|---|---|
| 言語 | Python | Go |
| レイテンシのオーバーヘッド | ~8ms/リクエスト | 11µs/リクエスト |
| スループットの検証 | 変動 | 5,000RPS |
| デプロイ | pip install + 設定 | 単一バイナリ、npx、またはDocker |
| 依存関係の面積 | 大きい推移的ツリー | 単一のコンパイル済みバイナリ |
| .pthの永続化リスク | あり(Python固有) | 該当なし |
| 設定アプローチ | YAMLファイル | Web UI + 環境変数 |
| ライセンス | オープンソース | オープンソース |
私がそれについてやっていること
3月24日のインシデントの後、私はLLMパイプライン内のPython依存関係をすべて監査しました。その作業だけで、ほぼ丸1日かかりました。
私はLLMルーティングの移行をBifrostへ始めました。移行は難しくありません。BifrostはOpenAI互換のAPI形式を使うため、ほとんどのクライアントコードは変更なしで済みます。主な作業は、プロバイダー設定をYAMLファイルからBifrostのセットアップへ移すことでした。
依存関係のリスクが許容できる、いくつかの重要ではない社内ツールでは、私は今もLiteLLMを使っています。しかし、本番トラフィックのクリティカルパス上にあるものについては、最小の攻撃対象面を持つコンパイル済みバイナリを用意するという選択肢は、別カテゴリーのトレードオフです。
このインシデントの後にLLMインフラを見直しているなら、私の助けになったリンクは以下です:
- Bifrost GitHub: https://git.new/bifrost
- Bifrost Docs: https://getmax.im/bifrostdocs
- Bifrost Homepage: https://getmax.im/bifrost-home
- Snyk Incident Report: https://snyk.io/articles/poisoned-security-scanner-backdooring-litellm/
- MITRE ATT&CK T1546.018: https://attack.mitre.org/techniques/T1546/018/
この投稿における数値や主張は、公開ドキュメント、Snykのインシデントレポート、BifrostのGitHubリポジトリから引用しています。ここに不正確な点があれば、コメントで指摘してください。私が修正します。




