{"repro_id":"REPRO-2026-00238","version":6,"title":"OpenBMB ChatDev unauthenticated path traversal file upload","repro_type":"security","status":"published","severity":"critical","description":"OpenBMB ChatDev through 2.2.0 contains a path traversal vulnerability allowing unauthenticated attackers to upload files outside the intended directory, which can lead to remote code execution. Reproduce: run ChatDev <=2.2.0, send an unauthenticated upload request with directory traversal sequences in the filename, and verify that the file is written outside the upload directory.","root_cause":"# Root Cause Analysis — CVE-2026-58166\n\n## Summary\n\nOpenBMB ChatDev through 2.2.0 exposes an unauthenticated file-upload path traversal in the product workflow server. The original `POST /api/uploads/{session_id}` route passes the raw multipart `filename` into `AttachmentService.save_upload_file()`, which joins it under a temporary upload directory without sanitization at the vulnerable commit. A remote unauthenticated attacker can first mint a session through the product `/ws` WebSocket, then upload a filename containing `../` path traversal segments. In this run, that primitive was chained on the original ChatDev product surface into code execution: the upload wrote a malicious workflow YAML outside the intended upload directory, and the product `POST /api/workflow/run` endpoint then executed attacker-controlled Python through ChatDev's real workflow `python` node runtime.\n\n## Impact\n\n- **Package/component affected:** OpenBMB/ChatDev workflow server, specifically `server/routes/uploads.py` and `server/services/attachment_service.py` (`AttachmentService.save_upload_file`). The demonstrated execution sink is the real workflow runtime reached by `server/routes/execute_sync.py` (`POST /api/workflow/run`), `runtime.sdk.run_workflow`, and `runtime/node/executor/python_executor.py`.\n- **Affected versions:** ChatDev `<= 2.2.0`, represented by vulnerable checkout `4fd4da603801766b14ad8788649cfc1ad21f99a6^` = `a6a5cda5560053136897aa44301eacc6c48d8168`.\n- **Fixed version/commit:** `4fd4da603801766b14ad8788649cfc1ad21f99a6`, which adds upload filename basename sanitization via `_safe_upload_filename()`.\n- **Risk level and consequences:** Critical. A remote unauthenticated attacker can write/delete files reachable by the server process via traversal. This run shows that the write primitive can be chained into product-level code execution by writing a workflow YAML to a traversed path and invoking ChatDev's workflow execution API to run attacker-controlled Python.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Remote code execution (`code_execution`) after unauthenticated path traversal upload.\n- **Reproduced impact from this run:** Full code execution on the original product API surface. Two vulnerable attempts created attacker-controlled marker files (`bundle/repro/rce_marker_vuln_1.txt` and `bundle/repro/rce_marker_vuln_2.txt`) from Python code executed by ChatDev's real workflow runtime after the malicious YAML was written via the unauthenticated upload path. Two fixed-commit attempts sanitized the filename, did not create the traversed YAML target, and returned `404` from `/api/workflow/run` because the target YAML file was not written.\n- **Parity:** `full`.\n- **Not demonstrated:** No privilege escalation or sandbox escape beyond the privileges of the ChatDev server process. The RCE demonstrated is arbitrary Python execution within the server's normal workflow execution environment.\n\n## Root Cause\n\nAt the vulnerable checkout, `AttachmentService.save_upload_file()` trusts `UploadFile.filename` and joins it directly onto a temporary directory:\n\n```python\nfilename = upload.filename or \"upload.bin\"\ntemp_dir = Path(tempfile.mkdtemp(prefix=\"mac_upload_\"))\ntemp_path = temp_dir / filename\nwith temp_path.open(\"wb\") as buffer:\n    ...\n```\n\nBecause Python path joining preserves `../` segments, a multipart filename such as `../../../../tmp/chatdev_cve58166_vuln_1_link.yaml` escapes the temporary upload directory. The product upload route is reachable remotely at `POST /api/uploads/{session_id}` and only requires a known session id. The reproduction obtains that session without authentication by opening the product `/ws` WebSocket; the server returns a fresh UUID session and records it in `active_connections`, after which `ensure_known_session(..., require_connection=False)` accepts uploads for that session.\n\nThe vulnerable function also removes `temp_path` in a `finally` block. A direct traversal write is therefore deleted after registration, but the reproduction uses a symlink at the traversed path. `open(\"wb\")` follows the symlink and writes the malicious YAML to the symlink target, while `unlink()` removes only the symlink. The resulting YAML file persists outside the upload directory. The product `/api/workflow/run` endpoint accepts an absolute `yaml_file` path and runs the referenced workflow; the uploaded YAML contains a single ChatDev `python` node, causing `PythonNodeExecutor` to execute attacker-supplied Python from `task_prompt` with `subprocess.run()`.\n\nThe fix commit `4fd4da603801766b14ad8788649cfc1ad21f99a6` adds `AttachmentService._safe_upload_filename()`, normalizing path separators and reducing the multipart filename to a safe basename before joining it with the temporary directory. In the fixed negative controls, the same traversal filename is returned as `chatdev_cve58166_fixed_*_link.yaml`, the `/tmp/...fixed_*.yaml` target is not created, and the follow-on workflow execution cannot find the malicious YAML.\n\nFix commit: https://github.com/OpenBMB/ChatDev/commit/4fd4da603801766b14ad8788649cfc1ad21f99a6\n\n## Reproduction Steps\n\n1. Run `bundle/repro/reproduction_steps.sh` with `bash`.\n2. The script:\n   - Reuses the prepared project cache at `<project_cache_dir>/repo` or clones `https://github.com/OpenBMB/ChatDev.git` if absent.\n   - Resolves the fixed commit and checks out `4fd4da603801766b14ad8788649cfc1ad21f99a6^` for vulnerable runs and `4fd4da603801766b14ad8788649cfc1ad21f99a6` for fixed runs.\n   - Starts the original product server via `server_main.py --host 127.0.0.1 --port <port>` and waits for `/health`.\n   - Opens the real `/ws` WebSocket to mint an unauthenticated session.\n   - Sends a real multipart `POST /api/uploads/{session_id}` request with a traversal filename targeting a symlink under `/tmp`.\n   - In vulnerable runs, persists a malicious workflow YAML outside the upload directory and then calls the real `POST /api/workflow/run` endpoint to execute attacker-controlled Python that writes a marker file under `bundle/repro/`.\n   - In fixed runs, verifies the same request is sanitized, the traversed YAML target is not created, and the workflow run fails closed with no marker file.\n3. Expected evidence:\n   - `bundle/repro/runtime_manifest.json` with `entrypoint_kind=api_remote`, `service_started=true`, `healthcheck_passed=true`, and `target_path_reached=true`.\n   - `bundle/repro/eval_summary.json` with both vulnerable code-execution attempts true and both fixed-blocked attempts true.\n   - `bundle/repro/result_vuln_1.json`, `result_vuln_2.json`, `result_fixed_1.json`, and `result_fixed_2.json`.\n   - `bundle/repro/rce_marker_vuln_1.txt` and `bundle/repro/rce_marker_vuln_2.txt` containing `RCE_OK_FROM_CHATDEV_WORKFLOW ...`.\n   - Runtime logs in `bundle/logs/`.\n\n## Evidence\n\nThe script was run twice consecutively and succeeded both times. The final run produced:\n\n```json\n{\n  \"vuln_attempts_code_execution\": [true, true],\n  \"vuln_attempts_persistent_yaml_write\": [true, true],\n  \"vuln_response_names\": [\n    \"../../../../../../../../../../../../../../../../tmp/chatdev_cve58166_vuln_1_link.yaml\",\n    \"../../../../../../../../../../../../../../../../tmp/chatdev_cve58166_vuln_2_link.yaml\"\n  ],\n  \"vuln_marker_contents\": [\n    \"RCE_OK_FROM_CHATDEV_WORKFLOW role=vuln attempt=1\\n\",\n    \"RCE_OK_FROM_CHATDEV_WORKFLOW role=vuln attempt=2\\n\"\n  ],\n  \"fixed_attempts_blocked_chain\": [true, true],\n  \"fixed_response_names\": [\n    \"chatdev_cve58166_fixed_1_link.yaml\",\n    \"chatdev_cve58166_fixed_2_link.yaml\"\n  ],\n  \"fixed_target_created\": [false, false],\n  \"confirmed\": true\n}\n```\n\nImportant artifact locations:\n\n- Main proof log: `bundle/logs/reproduction_steps.log`.\n- Vulnerable server logs: `bundle/logs/server_vuln_1.log`, `bundle/logs/server_vuln_2.log`.\n- Vulnerable driver logs: `bundle/logs/driver_vuln_1.log`, `bundle/logs/driver_vuln_2.log`.\n- Fixed server logs: `bundle/logs/server_fixed_1.log`, `bundle/logs/server_fixed_2.log`.\n- Fixed driver logs: `bundle/logs/driver_fixed_1.log`, `bundle/logs/driver_fixed_2.log`.\n- Structured runtime evidence: `bundle/repro/runtime_manifest.json`.\n- Marker files proving attacker-controlled Python execution: `bundle/repro/rce_marker_vuln_1.txt`, `bundle/repro/rce_marker_vuln_2.txt`.\n\nExample vulnerable result (`bundle/repro/result_vuln_1.json`): the upload response echoed the raw traversal filename, the traversed YAML target existed after upload, the symlink was removed, `/api/workflow/run` returned HTTP 200 with `final_message` `RCE_MARKER_WRITTEN role=vuln attempt=1`, and the marker contained `RCE_OK_FROM_CHATDEV_WORKFLOW role=vuln attempt=1`.\n\nExample fixed result (`bundle/repro/result_fixed_1.json`): the upload response name was sanitized to `chatdev_cve58166_fixed_1_link.yaml`, the traversed YAML target did not exist, `/api/workflow/run` returned `404` (`YAML file not found`), and no marker file was created.\n\n## Recommendations / Next Steps\n\n- Keep the fixed `_safe_upload_filename()` behavior: normalize both POSIX and Windows separators, strip directory components, and reject or replace empty/special names such as `.`, `..`, and blank filenames.\n- Consider adding defense in depth to `save_upload_file()` by resolving the final path and verifying it remains under the temporary directory before opening it.\n- Avoid following symlinks for temporary upload writes where possible, or create temp files with safe APIs that prevent symlink traversal.\n- Restrict `/api/workflow/run` from accepting arbitrary absolute YAML paths unless this is an explicit trusted-admin feature. Prefer resolving workflow files only under the configured workflow directory.\n- Add end-to-end regression tests that send multipart filenames with `../`, absolute paths, Windows separators, symlink targets, and the RCE chain pattern demonstrated here.\n- Upgrade users to a version containing commit `4fd4da603801766b14ad8788649cfc1ad21f99a6` or later.\n\n## Additional Notes\n\n- The reproduction is idempotent: the script removes old `/tmp/chatdev_cve58166_*` files and marker files before each attempt, chooses free local ports, and runs two vulnerable and two fixed attempts per invocation.\n- The script uses the original product server entrypoint (`server_main.py`) and real product routes. It does not reimplement the vulnerable upload handler or workflow execution sink.\n- A small `sitecustomize.py` shim is used only to avoid a Python import-cycle in the current environment when importing `runtime` submodules; it does not modify `AttachmentService`, routes, workflow execution logic, or the vulnerable/fixed behavior under test.\n","cve_id":"CVE-2026-58166","cwe_id":"CWE-22 (Improper Limitation of a Pathname to a Restricted Directory - Path Traversal)","source_url":"https://github.com/OpenBMB/ChatDev","package":{"name":"OpenBMB/ChatDev","ecosystem":"github","affected_versions":"ChatDev <= 2.2.0 (v2.2.0 is latest release dated Mar 23, 2026)"},"reproduced_at":"2026-07-06T08:21:50.253313+00:00","duration_secs":1707.0,"tool_calls":275,"handoffs":3,"total_cost_usd":9.382004419999998,"agent_costs":{"judge":0.034919500000000006,"repro":6.5467790599999995,"support":0.08164386,"vuln_variant":2.7186619999999997},"cost_breakdown":{"judge":{"gpt-5.4-mini":0.034919500000000006},"repro":{"accounts/fireworks/routers/glm-5p2-fast":1.77902406,"gpt-5.5":4.767754999999999},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.08164386},"vuln_variant":{"gpt-5.5":2.7186619999999997}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-06T08:22:23.046036+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":18770,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":10622,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":12766,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":11752,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":14220,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":1110,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1050,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1643,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":755,"category":"other"},{"path":"bundle/repro/result_vuln.json","filename":"result_vuln.json","size":1959,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":3910,"category":"log"},{"path":"bundle/repro/eval_summary.json","filename":"eval_summary.json","size":745,"category":"other"},{"path":"bundle/repro/result_vuln_1.json","filename":"result_vuln_1.json","size":3057,"category":"other"},{"path":"bundle/repro/result_fixed_1.json","filename":"result_fixed_1.json","size":1799,"category":"other"},{"path":"bundle/repro/real_product_rce_driver.py","filename":"real_product_rce_driver.py","size":6083,"category":"script"},{"path":"bundle/logs/server_vuln_1.log","filename":"server_vuln_1.log","size":0,"category":"log"},{"path":"bundle/logs/driver_vuln_1.log","filename":"driver_vuln_1.log","size":387,"category":"log"},{"path":"bundle/logs/server_vuln_2.log","filename":"server_vuln_2.log","size":0,"category":"log"},{"path":"bundle/logs/driver_vuln_2.log","filename":"driver_vuln_2.log","size":387,"category":"log"},{"path":"bundle/repro/result_vuln_2.json","filename":"result_vuln_2.json","size":3057,"category":"other"},{"path":"bundle/logs/server_fixed_1.log","filename":"server_fixed_1.log","size":0,"category":"log"},{"path":"bundle/logs/driver_fixed_1.log","filename":"driver_fixed_1.log","size":290,"category":"log"},{"path":"bundle/logs/server_fixed_2.log","filename":"server_fixed_2.log","size":0,"category":"log"},{"path":"bundle/logs/driver_fixed_2.log","filename":"driver_fixed_2.log","size":290,"category":"log"},{"path":"bundle/repro/result_fixed_2.json","filename":"result_fixed_2.json","size":1799,"category":"other"},{"path":"bundle/repro/rce_marker_vuln_1.txt","filename":"rce_marker_vuln_1.txt","size":49,"category":"other"},{"path":"bundle/repro/rce_marker_vuln_2.txt","filename":"rce_marker_vuln_2.txt","size":49,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":3729,"category":"other"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":4309,"category":"other"},{"path":"bundle/logs/vuln_variant_reproduction_steps.log","filename":"vuln_variant_reproduction_steps.log","size":13082,"category":"log"},{"path":"bundle/vuln_variant/eval_summary.json","filename":"eval_summary.json","size":2300,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":7081,"category":"documentation"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1036,"category":"other"},{"path":"bundle/logs/vuln_variant_driver_vulnerable.log","filename":"vuln_variant_driver_vulnerable.log","size":5335,"category":"log"},{"path":"bundle/logs/vuln_variant_driver_fixed.log","filename":"vuln_variant_driver_fixed.log","size":4803,"category":"log"},{"path":"bundle/vuln_variant/result_vulnerable.json","filename":"result_vulnerable.json","size":5334,"category":"other"},{"path":"bundle/vuln_variant/result_fixed.json","filename":"result_fixed.json","size":4802,"category":"other"},{"path":"bundle/vuln_variant/result_latest.json","filename":"result_latest.json","size":4802,"category":"other"}]}