Summary
The XAgent server's workspace file endpoint joins a caller-supplied file_name to a base directory and reads it with no path containment, so a file_name with parent-directory segments reads arbitrary files on the host. The server binds 0.0.0.0:8090, and its account gate is trivially satisfied: with the default send_email=False, POST /user/register returns an immediately available account and token (and a default admin/xagent-admin ships as well). The read therefore reaches host files such as /etc/passwd and XAgentServer/.env (database root credentials, JWT/SMTP secrets), outside the Docker tool sandbox. The interaction_id is also not scoped to the user (a secondary cross-user IDOR). Confirmed against the real handler path logic: file_name=../../../../../../etc/passwd returned the file contents.
Details
XAgentServer/application/routers/workspace.py (the /workspace/file handler): file_name is an attacker-controlled form field (file_name: str = Form(...) ~line 75), and every read branch (~lines 120, 128, 134, 139, 143, 146) does os.path.join(file_path, file_name) then open(...) / FileResponse(...) with no .. rejection or containment check.
Exposure: the server binds host="0.0.0.0", port=8090 (XAgentServer/application/core/envs.py ~lines 32 to 33). The gate user_is_available checks a user id and token, both self-obtainable: Email.send_email = False by default (envs.py ~line 65) makes /user/register set available=True immediately, returning a valid token; default_login=True also ships the documented admin/xagent-admin. Additionally, get_interaction filters by interaction_id only, not by user (XAgentServer/database/interface/interaction.py ~line 49), so interactions are cross-user accessible, but the traversal escapes the directory regardless of which interaction id is supplied.
PoC
POST /user/register (no email required -> instant available account + token)
POST /workspace/file with user_id, token, interaction_id=<any>, file_name=../../../../../../etc/passwd
Validated by driving the file() path-construction (XAgentServerEnv.base_dir + the documented record path + attacker file_name), which falls into the default text-read branch:
RESOLVED -> /etc/passwd
LEAKED FIRST LINE: root:x:0:0:root:/root:/bin/bash
LEAKED LINES: 51
Swapping the path for XAgentServer/.env, the MySQL data directory, or SSH keys yields full secret/host disclosure.
Impact
A self-registered (or default-admin) user of a network-reachable XAgent server reads arbitrary files on the host, including the application .env (database root credentials, JWT and SMTP secrets) and system files, outside the Docker tool sandbox the agent is supposed to be confined to. This is host-secret disclosure leading to broader compromise.
Remediation
Confine file_name to the interaction workspace: reject .. and absolute paths, and assert os.path.realpath(os.path.join(base, file_name)) is within the resolved workspace root before opening. Scope get_interaction to the authenticated user. Change the insecure defaults: do not auto-approve registration when email verification is disabled, and remove the shipped admin/xagent-admin default credentials (or force a change on first run).
Summary
The XAgent server's workspace file endpoint joins a caller-supplied
file_nameto a base directory and reads it with no path containment, so afile_namewith parent-directory segments reads arbitrary files on the host. The server binds0.0.0.0:8090, and its account gate is trivially satisfied: with the defaultsend_email=False,POST /user/registerreturns an immediately available account and token (and a defaultadmin/xagent-adminships as well). The read therefore reaches host files such as/etc/passwdandXAgentServer/.env(database root credentials, JWT/SMTP secrets), outside the Docker tool sandbox. Theinteraction_idis also not scoped to the user (a secondary cross-user IDOR). Confirmed against the real handler path logic:file_name=../../../../../../etc/passwdreturned the file contents.Details
XAgentServer/application/routers/workspace.py(the/workspace/filehandler):file_nameis an attacker-controlled form field (file_name: str = Form(...)~line 75), and every read branch (~lines 120, 128, 134, 139, 143, 146) doesos.path.join(file_path, file_name)thenopen(...)/FileResponse(...)with no..rejection or containment check.Exposure: the server binds
host="0.0.0.0", port=8090(XAgentServer/application/core/envs.py~lines 32 to 33). The gateuser_is_availablechecks a user id and token, both self-obtainable:Email.send_email = Falseby default (envs.py~line 65) makes/user/registersetavailable=Trueimmediately, returning a valid token;default_login=Truealso ships the documentedadmin/xagent-admin. Additionally,get_interactionfilters byinteraction_idonly, not by user (XAgentServer/database/interface/interaction.py~line 49), so interactions are cross-user accessible, but the traversal escapes the directory regardless of which interaction id is supplied.PoC
Validated by driving the
file()path-construction (XAgentServerEnv.base_dir+ the documented record path + attackerfile_name), which falls into the default text-read branch:Swapping the path for
XAgentServer/.env, the MySQL data directory, or SSH keys yields full secret/host disclosure.Impact
A self-registered (or default-admin) user of a network-reachable XAgent server reads arbitrary files on the host, including the application
.env(database root credentials, JWT and SMTP secrets) and system files, outside the Docker tool sandbox the agent is supposed to be confined to. This is host-secret disclosure leading to broader compromise.Remediation
Confine
file_nameto the interaction workspace: reject..and absolute paths, and assertos.path.realpath(os.path.join(base, file_name))is within the resolved workspace root before opening. Scopeget_interactionto the authenticated user. Change the insecure defaults: do not auto-approve registration when email verification is disabled, and remove the shippedadmin/xagent-admindefault credentials (or force a change on first run).