MLflow ve verzích 2.1.0 až 3.14.x ignoroval zákaz pickle u modelů statsmodels
Bezpečnostní volba v MLflow měla zastavit načítání modelů uložených pomocí pickle, integrace statsmodels ji ale nekontrolovala. Upravený modelový artefakt proto mohl při načtení spustit kód i s nastavením MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False. Verze 3.15.0 kontrolu doplnila a projekt podrobnosti zveřejnil 1. září.

Integrace statsmodels přeskočila společnou pojistku
MLflow ukládá modely v několika takzvaných flavors, tedy integracích pro konkrétní knihovny a formáty. Stejný model pak lze načíst přímo přes jeho integraci nebo společnou funkcí mlflow.pyfunc.load_model(). U modelů z knihovny statsmodels vedou obě cesty nakonec do funkce statsmodels.iolib.load_pickle().
Formát pickle není obyčejný balík dat. Při obnově objektu může zavolat funkci a tím spustit kód. Dokumentace Pythonu proto říká, že se mají načítat jen důvěryhodná data. Statsmodels u své funkce varuje stejně: model z nedůvěryhodného nebo neověřeného zdroje se nemá rozbalovat.
MLflow má pro takové nasazení proměnnou MLFLOW_ALLOW_PICKLE_DESERIALIZATION. Hodnota False má načítání pickle zastavit. Integrace pro scikit-learn, PyTorch nebo pmdarima ji kontrolovaly, statsmodels ne. Ve verzi 3.14.0 funkce _load_model() rovnou zavolá smio.load_pickle(path). Hodnota bezpečnostní volby se v souboru vůbec nečte.
Kód se spustil až při načtení artefaktu
Podmínka útoku je důležitá. Nestačí otevřít webové rozhraní nebo si prohlédnout kartu modelu. Útočník musí dostat upravený soubor MLmodel a škodlivý pickle do úložiště, ke kterému má oběť přístup. Potom musí nějaký proces zavolat mlflow.pyfunc.load_model() nebo mlflow.statsmodels.load_model() nad tímto artefaktem.
Bezpečnostní hlášení GHSA-gqvg-gmmx-x4hm upozorňuje, že výchozí server MLflow nemá zapnuté přihlašování. Z toho ale neplyne, že každý server na internetu dovolí komukoli podstrčit a načíst model: rozhodují práva k úložišti, způsob nasazení a to, kdo operaci načtení spustí. Hlášení hodnotí chybu známkou 8,8 z deseti a uvádí nutnou součinnost uživatele. Číslo CVE zatím přidělené nemá.
Rozsah ve stejném záznamu začíná verzí 2.1.0 a končí těsně před 3.15.0. Jako první opravenou verzi uvádí jen 3.15.0, nikoli některé vydání řady 2.x. Správci starší hlavní řady tak nemají v hlášení uvedenou opravenou větev, na které by mohli zůstat.
Verze 3.15.0 hlídá oba způsoby načtení
Opravu přinesl návrh změny #24686, přijatý 27. července. Kontrola teď stojí přímo na začátku _load_model(), tedy před voláním statsmodels. Při zakázané deserializaci vyhodí MLflow výjimku a k pickle se nedostane. Test zkouší obě veřejné cesty načtení, přímou funkci statsmodels i společné pyfunc.load_model().
Kód zachovává stejnou výjimku jako ostatní integrace MLflow: kontrolu nepoužije uvnitř Databricks Runtime a v prostředí Databricks Model Serving. Nejde o přehlédnutou větev nové opravy, ale o záměr, který opravný commit přebírá ze sourozeneckých integrací.
MLflow 3.15.0 vyšlo 31. července. Poznámky k vydání opravu uvádějí jedním řádkem mezi běžnými opravami modelů, bez známky závažnosti a bez odkazu na neveřejné hlášení. Veřejný popis chyby přibyl 1. září, tedy o 32 dní později. Oprava tak byla měsíc dostupná, ale její dopad se z poznámek k vydání určit nedal.
Původ modelu zůstává po aktualizaci rozhodující
Aktuální vydání 3.15.2 z 26. srpna už opravu obsahuje. Aktualizace zavře konkrétní mezeru v integraci statsmodels, ale z pickle neudělá bezpečný formát. Modelové artefakty dál patří do stejné kategorie jako spustitelný kód: úložiště potřebuje řízení zápisu a původ modelu se musí ověřit před načtením.
Přepínač s hodnotou False má smysl jako druhá pojistka. Tahle chyba ukazuje jeho mez: globálně pojmenovaná bezpečnostní volba chránila jen integrace, které ji skutečně zkontrolovaly. Vlastní nebo méně obvyklé loadery je proto potřeba prověřovat podle kódu, ne podle názvu proměnné.