Webhooky MLflow vydaly obsah z vnitřní sítě i nepřihlášenému útočníkovi
Výchozí server MLflow nechává rozhraní pro webhooky bez přihlášení a jeho pojistku proti SSRF šlo obejít obyčejným přesměrováním. Útočník tak mohl serveru poručit, ať sáhne na adresu ve vnitřní síti, a odpověď dostal zpátky celou. Opravu vydal projekt 31. července ve verzi 3.15.0, číslo CVE k ní přibylo o sedmnáct dní později.

K čemu jsou v MLflow webhooky
MLflow je otevřený nástroj, do kterého si týmy zapisují běhy trénování, naměřené hodnoty a hotové modely. Vlastní server se spouští jedním příkazem mlflow server a ve výchozím nastavení jede nad databází SQLite a bez jakéhokoli přihlašování. Přihlášení přidá teprve volitelný modul, který se zapíná přepínačem --app-name basic-auth.
Webhook je odchozí oznámení: jakmile v registru vznikne nová verze modelu nebo se změní její stav, server sám zavolá adresu, kterou mu k tomu někdo předem uložil. Patří k tomu i zkušební rozhraní POST /api/2.0/mlflow/webhooks/{id}/test, které oznámení odešle na povel a volajícímu vrátí stavový kód i celé tělo odpovědi, kterou od té adresy dostalo. To druhé je užitečné, dokud jde uhlídat, kam server chodí.
Pojistka z února kontrolovala adresu, ne spojení
Že se webhookem dá ukázat dovnitř vlastní sítě, projekt věděl. V únoru 2026 přibyla funkce _validate_webhook_url: propustí jen schéma https, přeloží jméno hostitele na adresy a odmítne každou, která není veřejná. Tím padá smyčka, privátní rozsahy i adresa metadatové služby cloudu, kde leží dočasné přístupové údaje stroje. Poprvé to bylo ve verzi 3.10.0 z 20. února.
Kontrola ale výsledek překladu zahodila. Vlastní požadavek se sestavil znovu z té samé adresy a jméno hostitele se překládalo podruhé. Mezi ověřením a spojením tak zůstala mezera, a to hned dvojí. Jméno se dá mezi oběma překlady přepsat, čemuž se říká DNS rebinding. A pak je tu jednodušší cesta: odesílání sledovalo přesměrování, takže stačilo, aby ověřená veřejná adresa odpověděla kódem 302 a poslala server jinam. Cíl přesměrování už nikdo neověřoval.
Na serveru bez přihlašování zvládne obojí kdokoli, kdo se k němu dostane po síti: webhook si založí sám, sám ho zkušebním voláním odpálí a odpověď z vnitřní sítě si přečte v návratu. Hlášení GHSA-7gwp-5pfp-969j vede chybu jako CVE-2026-64849 se známkou 9,3 z deseti, tedy kritickou, a týká se všech verzí před 3.15.0. Oprava, která díru nezavře celou, přitom v oboru není rarita: Keras řešil čtení cizích souborů přes odkazy v souborech HDF5 třikrát za půl roku.
Oprava kontroluje protějšek až po připojení
Opravu poslal Pat Sukprasert v návrhu změny #24258 a projekt ji přijal 2. července. Přibyl s ní soubor mlflow/webhooks/ssrf.py o 144 řádcích. Je v něm adaptér, který se u každého spojení podívá, s jakou adresou se soket opravdu spojil – hned po návratu z connect(), dřív než proběhne šifrované podání ruky. Mezi kontrolou a přenosem už žádný druhý překlad jména není, takže není co přepsat, a protože se ověřuje každé spojení zvlášť, projde tou kontrolou i přesměrování.
Ke stejné změně patří jeden méně nápadný řádek v souboru delivery.py: session.trust_env = False. Kdyby si knihovna vzala nastavení proxy serveru z prostředí, ověřovala by protějšek proxy, ne cíle. Komentář nad ním to říká rovnou.
V poznámkách k vydání je to jeden řádek ze 103
Ven šla oprava 31. července ve verzi 3.15.0 a poznámky k vydání ji jmenují: „Fix DNS-rebinding SSRF bypass in webhook delivery (#24258, @PattaraS)“. Stojí ovšem mezi ostatními změnami v oddílu nazvaném „Other Assorted Features & Improvements“ a nese štítek „[Model Registry]“, ne bezpečnostní. Napočítali jsme v tom oddílu 103 položek a slovo „security“ ani jediné číslo CVE se v celých poznámkách neobjeví. Správce, který si je přečetl, tedy neměl z čeho poznat, že tahle položka spěchá víc než ostatní.
Druhá strana téže věci: od prvního neveřejného hlášení 12. června do přijaté opravy uplynulo dvacet dní a text, který projekt 17. srpna zveřejnil, popisuje chybu podrobně a jmenuje oba nálezce – toho, kdo ji ohlásil neveřejně, i toho, kdo tutéž věc 26. června popsal v otevřeném hlášení #24179. Odstup mezi opravou a jejím pojmenováním má navíc dobrý důvod: dokud aktualizace není venku, popis chyby pomáhá útočníkovi. Stejné pořadí zvolilo v srpnu i Open WebUI, jen s osmidenním odstupem.
Další dvě hlášení téhož dne míří jinam
MLflow zveřejnil 17. srpna ještě dvě hlášení a obě mají opravu v téže verzi 3.15.0. Dopadají ale na jiné nasazení, totiž na to, kde přihlašovací modul zapnutý je. CVE-2026-69148 se známkou 7,1 dovolovalo přihlášenému uživateli založit verzi modelu, která ukazuje do adresáře s výstupy cizího běhu. Ty soubory si pak mohl přečíst, protože kontrola se ptala jen na to, jestli cesta leží uvnitř toho adresáře. Na právo k němu se neptala. CVE-2026-69146 se známkou 6,5 popisuje rozhraní log-inputs, které v seznamu hlídaných volání přihlašovacího modulu úplně chybí. Zápis záznamu do cizího běhu tudy prošel, zatímco zápis naměřené hodnoty do téhož běhu skončil kódem 403.
Název opravy říká míň, než je uvnitř
Pro provoz z toho plyne krátká věta: verze 3.15.0 z 31. července nebo novější, přičemž poslední vydání 3.15.1 je z 3. srpna. A pak je tu jedna věc, kterou název opravy zamlčuje. Návrh změny i řádek v poznámkách mluví o DNS rebindingu, tedy přesně o tom, co popsalo veřejné hlášení. Cesta přes přesměrování, kterou to neveřejné vede jako spolehlivější, v nich nepadne ani slovem, přestože ji nová kontrola zavírá taky. Kdo si podle názvů oprav vybírá, co aktualizovat dřív, přečte si tady míň, než v té změně opravdu je.
Zdroje
- Hlášení GHSA-7gwp-5pfp-969j v repozitáři projektu MLflow
- Návrh změny #24258 a hlášení #24179 tamtéž
- Poznámky k vydání MLflow 3.15.0 a poznámky k vydání 3.10.0
- Zdrojové soubory mlflow/webhooks/ssrf.py a mlflow/server/auth/__init__.py ve verzi 3.15.0