Starletteの脆弱性として2026年に最も広く影響したのが、CVE-2026-48710(通称BadHost)です。FastAPIの土台であるStarletteが、HTTPのHostヘッダを検証しないままrequest.urlを組み立てていたため、ヘッダを細工するだけでパスを見て認可するミドルウェアを素通りできました。2026年9月2日には米国CISAの悪用確認済み脆弱性カタログ(KEV)へ追加されています。ここでは仕組みを手元で再現した結果を出発点に、自社のアプリが該当するかの判定、FastAPIの依存制約で修正版へ上げられないケース、恒久対策の書き方までを2026年10月時点の一次情報で整理します。
まとめ:Starletteの脆弱性CVE-2026-48710で先に確かめる3つの条件
結論から書きます。該当するかどうかは、(1)Starletteが1.0.0以下か、(2)request.url.pathやrequest.urlを使って認可や遮断を判断するミドルウェアがあるか、(3)RFC準拠のリバースプロキシを通さずにASGIサーバーへ直接届く経路があるか、の3条件で決まります。3つすべてが揃うと、認証なしで保護対象のエンドポイントへ到達される状態です。
対応はバージョンを上げるのが先。ただし修正版の1.0.1で止めず、2026年に続けて公表された4件のCVEまで塞がる1.3.1以上(2026年10月時点の最新は1.7.0)を狙ってください。もう1つの落とし穴がFastAPI側で、0.132.1以前は依存定義がstarletteを1.0.0未満に固定しているため、Starletteだけを上げようとしても依存解決が失敗します。FastAPIを0.133.0以上へ同時に上げる必要がある、というのが本記事の芯です。脆弱性の一般論とCVE番号の読み方は脆弱性の種類と発見から修正までの実務を前提にします。
CVE-2026-48710の仕組みとHostヘッダでパスがずれる理由
まず、何が起きていたのかを修正コミットの差分から確認します。
修正前のStarletteがrequest.urlをHostヘッダから組み立てる手順
Starletteはrequest.urlを作るとき、スキームとHostヘッダの値とリクエスト行のパスを文字列として連結し、それを改めてURLとして解析していました。発見者であるX41 D-Secのアドバイザリ X41-2026-002の説明では、GET /fooにHost: example.com/abc?bar=を付けると、連結結果はhttp://example.com/abc?bar=/fooになります。これを解析し直すと、パスは/abc、本来の/fooはクエリ文字列の一部です。
一方、ルーティングはリクエスト行の生のパス(ASGIのscope["path"])で行われるため、実際に実行されるのは/fooのエンドポイント。ミドルウェアが見るrequest.url.pathと、実行されるルートが食い違う。この食い違いが脆弱性の本体です。修正コミット764dab0では、Hostの値を英数字・ドット・ハイフンとポート番号(IPv6は角括弧)に限る正規表現で検証し、外れた場合はヘッダを捨ててscope["server"]のホスト名へフォールバックする形に変わりました。
KEVの名称はスマグリングでも実体はHostヘッダ検証不備という整理
情報を集めると、呼び名が揃っていないことに気づきます。CISAのKEVカタログでの名称は「HTTP Request/Response Smuggling Vulnerability」で、NVDも分類にCWE-444(HTTPリクエストスマグリング)を付けています。ところが開発元のGitHubアドバイザリGHSA-86qp-5c8j-p5mrの題名は「Host header validationの欠如がrequest.url.pathを汚染し、パスベースのセキュリティ検査を迂回させる」です。
実装の観点では後者で理解してください。プロキシとサーバーの間で1つの通信を2つに切り分けるいわゆるリクエストスマグリングとは手口が異なり、単一のリクエストのヘッダ1つで成立する手口です。KEVの名称だけで「プロキシの設定問題」と受け取ると、アプリ側のミドルウェアを点検しないまま終わる恐れがあります。CVE番号とNVD・KEVの関係そのものはCVEの仕組みとCVSS・CWE・NVDとの違いで解説しています。
CVSS 6.5のModerateなのにKEV入りして期限14日になった経緯
時系列を並べると、評価の割れ方が見えてきます。GHSAの公開と1.0.1のリリースは2026年5月21日、NVDの登録は5月26日で、深刻度はCVSS 3.1で6.5(Moderate)。発見者のX41はCVSS 4.0で7(High)と評価し、監査を支援したOSTIFも5月26日の公表文で「アドバイザリの評価は下流での深刻さを大きく過小評価している」と書きました。
そして2026年9月2日、CISAは本CVEをKEVへ追加し、連邦機関向けの対応期限を9月16日(14日後)に設定しています。CISAが付けたSSVCの判定は、悪用が「active」、自動化が「yes」。KEVの説明文は、LiteLLMのコマンド注入であるCVE-2026-42271(CVSS 3.1で8.8・2026年6月8日にKEV追加)と連鎖し得るとも記しています。点数は中程度でも、認証の手前を崩す脆弱性は後ろに控える欠陥と組み合わさって実害に変わる、という典型です。KEVの期限がどう決まるかはKEVの是正期限を4変数で決めるBOD 26-04の仕組みで扱っています。
手元で再現する:Starlette 1.0.0と1.7.0で同じリクエストを比べる
サーバーを起動せず、ASGIアプリを直接呼び出す形で再現しました。
ネットワーク不要でASGIアプリを直接呼ぶ再現スクリプトの全体
次のスクリプトは、/healthだけを公開し、それ以外を403で止めるミドルウェアをrequest.url.pathで書いた例です。Hostヘッダを差し替えて/adminを2回呼びます。依存はStarlette本体だけで、pip install "starlette==1.0.0"のように版を固定した仮想環境で実行してください。
import asyncio
import starlette
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import PlainTextResponse
from starlette.routing import Route
class PathAuth(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# 危険な書き方:request.url.path で認可を判定している
if request.url.path in ("/", "/health"):
return await call_next(request)
return PlainTextResponse("Forbidden", status_code=403)
async def health(request):
return PlainTextResponse("ok")
async def admin(request):
return PlainTextResponse("secret")
app = Starlette(routes=[Route("/health", health), Route("/admin", admin)],
middleware=[Middleware(PathAuth)])
async def call(host):
scope = {"type": "http", "asgi": {"version": "3.0"}, "http_version": "1.1",
"method": "GET", "scheme": "http", "path": "/admin", "raw_path": b"/admin",
"query_string": b"", "root_path": "", "headers": [(b"host", host)],
"server": ("127.0.0.1", 8000), "client": ("127.0.0.1", 50000)}
status = {}
async def receive():
return {"type": "http.request", "body": b"", "more_body": False}
async def send(msg):
if msg["type"] == "http.response.start":
status["code"] = msg["status"]
await app(scope, receive, send)
return status["code"]
for h in [b"example.com", b"example.com/health?x="]:
print(starlette.__version__, h.decode(), asyncio.run(call(h)))
2026年10月10日に実行した結果は次のとおりです。1.0.0では、細工したHostを付けた2回目だけが200になり、/adminの中身が返りました。1.7.0では2回とも403で、修正後は不正なHostが無視されてサーバー側のホスト名に置き換わるため、ミドルウェアは本来のパスを見ています。
1.0.0 example.com 403
1.0.0 example.com/health?x= 200
1.7.0 example.com 403
1.7.0 example.com/health?x= 403
FastAPIで3通りの認可の書き方を比べた実測結果と読み方
同じ手順をFastAPI 0.133.0とStarlette 1.0.0の組み合わせで、認可の書き方だけを変えて試しました。比べたのは、@app.middleware("http")の中でrequest.url.pathを見る書き方、同じ場所でrequest.scope["path"]を見る書き方、そしてエンドポイントにDependsで認可関数を付ける書き方の3つです。
| 認可の書き方 | 通常のHost | 細工したHost |
|---|---|---|
| request.url.pathで判定 | 403 | 200(迂回) |
| scope[“path”]で判定 | 403 | 403 |
| Dependsで判定 | 403 | 403 |
読み方は明快です。脆弱なのはStarlette 1.0.0以下と、request.urlからパスを取り出して判断するコードの組み合わせ。scope["path"]はリクエスト行から来る値なのでHostの影響を受けず、Dependsはルーティングが確定した後のエンドポイント単位で動くため、パスの解釈がずれても認可は外れません。BadHostの解説サイトも、FastAPI標準のDepends()とSecurity()による認可は安全側だと明記しています。
影響を受ける条件をバージョンと認可処理の書き方の2軸で判定する手順
再現結果から、判定は2軸に分けられます。片方だけでは結論が出ないので、両方を確認してください。
影響範囲はStarlette 0.8.3から1.0.0まで・修正は1.0.1以降
X41のアドバイザリによれば、影響を受けるのは0.8.3以上1.0.1未満で、0.8.3は2018年11月の公開。ほぼすべての現役環境が対象に入ります。まず実行環境に入っている版を確かめます。仮想環境ごとに違う版が入っていることがあるので、本番と同じイメージやロックファイルで確認してください。
python -c "import starlette, fastapi; print('starlette', starlette.__version__, '/ fastapi', fastapi.__version__)"
pip show starlette fastapi
uv pip list | grep -i -E "starlette|fastapi"
注意したいのは、OSのパッケージで入れたStarletteです。Debianのセキュリティトラッカーでは、bookwormは0.26.1-1+deb12u1、trixieは0.46.1-3+deb13u2でバックポートにより修正済み(DSA-6302-1)。版番号だけを見れば1.0.0未満でも、パッケージのリビジョンで直っている場合があります。逆に、OS側を更新してもpipやuvで作った仮想環境の中は別物で、何も変わりません。
request.url.pathを認可に使うミドルウェアをgrepで洗い出す手順
版が該当したら、次はコード側です。危ないのは、ミドルウェアでrequest.urlから取ったパスを使い、許可リスト・拒否リスト・CSRFの除外・レート制限・課金判定などを決めている箇所。自作のコードと、インストール済みの依存パッケージの両方を検索します。
grep -rn --include="*.py" -E "request\.url(\.path)?|URL\(scope" ./app
grep -rln --include="*.py" -E "request\.url\.path" .venv/lib
2行目はライブラリ側を見る検索で、Windowsの仮想環境なら.venv/Libに読み替えてください。ヒットした箇所が「表示用のURL生成」なのか「通してよいかの判断」なのかを1件ずつ仕分けます。前者は影響が限られ、後者は即時の修正対象です。社内で仕分けの判断がつかない、あるいは外部に公開しているAPIが多く手が回らない場合は、脆弱性診断・セキュリティ診断で外側から到達可能性を確かめる方法もあります。
vLLMやLiteLLM、MCPサーバーなどAI基盤が狙われやすい理由
OSTIFとBadHostの公表文が名指ししているのは、vLLM、LiteLLM、OpenAI互換のプロキシ、MCPサーバー、エージェント基盤です。これらはFastAPIやStarletteの上に作られ、/v1/modelsや管理用の/adminのようにパスの前方一致で保護範囲を決める実装が多い。BadHostの解説サイトはさらに、MCPの仕様がOAuthの検出用エンドポイントを認証なしで公開させるため、Hostに差し込む「公開パス」が確実に手に入ると指摘しています。
自前でLLMの推論サーバーやMCPサーバーを立てている場合は、上流のプロジェクトが修正版のStarletteを要求しているか、コンテナイメージの中の版を必ず確認してください。推論サーバー側の構成はvLLMの仕組みと使い方、PythonでのMCPサーバー実装はFastMCP 4の移行判断で扱っています。
修正手順:FastAPI 0.132以前はStarletteだけを上げられない
ここからが、日本語の解説でほとんど触れられていない部分です。修正版のStarletteを入れようとして、依存解決で止まる環境が少なくありません。
FastAPIの依存制約がstarlette 1.0未満で止まる版の境界を確かめる
FastAPIは長くStarletteの上限を固定してきました。PyPIのFastAPIの各版の依存定義を確かめると、0.115.12は0.47未満、0.128.0は0.51未満、0.128.8から0.132.1までは1.0.0未満です。上限が外れたのは2026年2月24日公開の0.133.0で、0.134.0以降は下限0.46.0のみになりました。2026年10月8日公開の最新0.143.0も同じ定義です。
つまりFastAPI 0.132.1以前のままstarletteを1.0.1以上に指定しても、依存の矛盾で解決できません。手元のuvで確かめた結果が次で、fastapi==0.132.1とstarlette>=1.0.1は「解なし」、fastapi==0.133.0とstarlette==1.7.0、fastapi==0.143.0とstarlette>=1.3.1はどちらも解決しました。直すにはFastAPIとStarletteを1回の変更で同時に上げます。
# pyproject.toml の dependencies 例
dependencies = [
"fastapi>=0.133.0",
"starlette>=1.3.1",
]
# uv を使う場合(ロックファイルも更新される)
uv add "fastapi>=0.133.0" "starlette>=1.3.1"
starletteを直接の依存に書いておくのは、FastAPIの依存定義が下限0.46.0のままだからです。FastAPIだけを上げても、ロックファイルに残った古いStarletteが据え置かれる場合があります。FastAPIの基本構造とバージョンの追い方はFastAPIを最短で動かす手順を参照してください。
1.0.1で止めず1.3.1以上へ上げる理由と2026年の後続CVE4件
CVE-2026-48710だけなら1.0.1で塞がります。しかしStarletteのセキュリティアドバイザリ一覧を見ると、1.0.1の公開後に4件が続きました。1.0.1のまま止めると、これらが残ります。
| CVE | 修正版 | 深刻度 |
|---|---|---|
| CVE-2026-48817 | 1.1.0 | Medium 5.3 |
| CVE-2026-48818 | 1.1.0 | High 7.5 |
| CVE-2026-54282 | 1.3.0 | Low 3.7 |
| CVE-2026-54283 | 1.3.1 | High 7.5 |
内容は順に、HTTPEndpointで任意のHTTPメソッド名が属性として呼ばれる問題、Windows上のStaticFilesでUNCパス経由のSSRFとNTLM認証情報の漏えい、リクエストパスがホスト部に連結されてrequest.url.hostnameが汚染される問題、そしてapplication/x-www-form-urlencodedのフォームで上限設定が効かずDoSになる問題です。とくにCVE-2026-54283はフォームを受けるほぼすべてのAPIに関わります。下限は1.3.1、作業するならPyPIのStarletteで最新版(2026年10月時点は1.7.0・Python 3.10以上が必要)を確かめて、そこへ揃えるのが手戻りの少ない選び方。
更新を定例へ組み込みStarletteの次のCVEを取りこぼさない運用
Starletteは2026年5月21日の1.0.1から6月12日の1.3.1まで、約3週間でセキュリティ修正を含む版を4回出しました。依存更新の起票を自動化し、セキュリティ修正を含む更新だけはテストが通った時点で即日マージする、という線を先に決めておきます。更新の起票はdependabot.ymlの設定とRenovateとの違いの手順でプルリクエストとして届く形にでき、深刻度で止める基準はCVSSの見方と計算方法が判断材料になります。
恒久対策としてパスで認可しない設計へ寄せる認可処理の書き換え方
バージョンを上げれば今回の穴は塞がります。それでも、パス文字列で認可を決める設計そのものは脆いまま残ります。同じ型の欠陥は別のフレームワークでも起き得るからです。
scope[“path”]で判定し直す書き換えとDependsへ寄せる判断基準
ミドルウェアで判定を続けるなら、パスはrequest.url.pathではなくrequest.scope["path"]から取ります。ただしこれは応急処置に近い位置づけです。新しく書くなら、認可はエンドポイントにDependsで結び付けるほうを選んでください。
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
app = FastAPI()
PUBLIC_PATHS = {"/", "/health"}
# 応急:ミドルウェアで判定を続けるなら scope["path"] を使う
@app.middleware("http")
async def guard(request: Request, call_next):
if request.scope["path"] in PUBLIC_PATHS:
return await call_next(request)
if request.headers.get("x-api-key") != "change-me":
return JSONResponse({"detail": "Forbidden"}, status_code=403)
return await call_next(request)
# 推奨:認可をエンドポイント単位の依存関数として付ける
def require_admin(request: Request):
if request.headers.get("x-api-key") != "change-me":
raise HTTPException(status_code=403)
@app.get("/admin", dependencies=[Depends(require_admin)])
def admin():
return {"ok": True}
ミドルウェア側ではHTTPExceptionを投げず、応答オブジェクトを直接返している点に注目してください。FastAPI公式のミドルウェアの説明にあるとおり、ミドルウェアはルーティングより手前ですべてのリクエストに割り込む仕組みで、エンドポイント向けの例外処理とは経路が異なります。判断の基準は単純で、「どのパスを守るか」をミドルウェアの文字列比較で表しているなら、ルート定義側へ移す対象です。
リバースプロキシでHostヘッダを正規化しても完全な対策にならない理由
nginx・Caddy・Traefik・HAProxyのようにRFCに沿ってHostを検証するリバースプロキシを前に置けば、不正な値はそこで弾かれます。BadHostの解説サイトも有効な緩和策として挙げています。
それでも完全な対策にはなりません。理由は3つあります。開発・検証環境やコンテナ間の内部通信では、ASGIサーバーへ直接届く経路が残りがちなこと。X-Forwarded-Hostのように攻撃者が値を決められるヘッダを信頼する設定にしていると、プロキシの検証を素通りすること。そしてプロキシの設定変更1つで防御が消える、という依存関係ができることです。Uvicornを直接公開する構成と本番での前段の置き方はUvicornの基礎から本番デプロイまでで整理しています。
更新時期の判断:即日上げる構成と、次の定例に回してよい構成の線引き
最後に、対応の速さを条件で言い切ります。KEV入りした以上、悪用の前提で考えるのが妥当です。
即日で上げるのは、Starletteが1.0.0以下で、かつrequest.url系のパスで認可するミドルウェアがあり、ASGIサーバーへ直接届く経路が1つでもある構成。LLMの推論サーバーやMCPサーバー、管理APIを外部に出している場合はこれに当たると考えてください。FastAPIが0.132.1以前なら、FastAPIの更新を含めて同じ日に作業します。
次の定例に回してよいのは、認可がすべてDependsか外部の認証基盤で行われ、grepでrequest.url.pathの判定箇所が見つからない構成です。ただし前述の後続CVE、特にフォームのDoSは別の問題として残るため、定例で1.3.1以上へ上げることは見送らないでください。見送ってよいのは、Starletteを含むサービスが社内ネットワークに閉じ、前段のプロキシでHostを固定していると設定ファイルで確認できたときだけで、その根拠はチケットに残します。
よくある質問
検索の多い5つの質問に、2026年10月時点の一次情報で答えます。
FastAPIを使っていればStarletteの脆弱性の影響を受けますか?
FastAPIはStarletteの上に作られているため、Starletteが1.0.0以下なら版の条件には該当します。ただし実際に迂回されるのは、ミドルウェアでrequest.url.pathなどを見て認可しているアプリに限られます。手元の実測でも、Dependsで認可したエンドポイントは細工したHostでも403のままでした。
CVE-2026-48710はStarletteのどのバージョンで修正されましたか?
2026年5月21日公開の1.0.1で修正されました。不正なHostを無視してサーバー側のホスト名へ切り替える実装に変更されています。ただし1.0.1以降も4件のCVEが続いたため、1.3.1以上、可能なら2026年10月時点の最新である1.7.0へ上げるのが妥当です。DebianのOSパッケージはbookworm・trixieともバックポートで修正済みです。
Starletteを1.0.1以上に指定するとインストールが失敗するのはなぜですか?
FastAPI 0.132.1以前が、依存定義でStarletteを1.0.0未満に固定しているためです。上限が外れたのは2026年2月24日公開のFastAPI 0.133.0からです。FastAPIを0.133.0以上へ、Starletteを1.3.1以上へ、1回の変更で同時に上げれば解決します。
KEVに追加されたことは日本企業にも関係がありますか?
KEVの対応期限が法的に拘束するのは米国の連邦機関ですが、「実際に悪用が確認された」という事実はどの組織にも当てはまります。本CVEの期限は追加から14日後の2026年9月16日でした。優先度を決める材料として、KEV入りの有無はCVSSの点数より強い信号として扱ってください。
リバースプロキシを置いていれば対応は不要ですか?
RFCに沿ってHostを検証するプロキシは有効な緩和策ですが、対応不要にはなりません。検証環境や内部通信の直接経路、X-Forwarded-Hostの信頼設定、プロキシの設定変更といった抜け道が残るからです。
関連記事
- 脆弱性とは?種類・CVE/CVSSの仕組みと発見から修正までの実務を解説:脆弱性対応の全体像
- BOD 26-04とは?KEVの是正期限を4変数で決める仕組みと自社運用への移し方:KEVの期限の決まり方
- FastAPIとは|最短で動かす手順とasync/defの使い分け【2026年版】:FastAPIの基本構造
- Djangoの脆弱性への対応|セキュリティリリースの追い方と標準で防げない穴:Pythonフレームワークの脆弱性運用
- 脆弱性診断とは?種類・費用相場・進め方と外注時の判断基準を解説:外部診断の進め方