Skip to content

Path traversal in /workspace/file reads arbitrary host files (self-registerable, escapes the Docker sandbox) #429

Description

@geo-chen

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions