# Root Cause Analysis — CVE-2026-58166

## Summary

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

## Impact

- **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`.
- **Affected versions:** ChatDev `<= 2.2.0`, represented by vulnerable checkout `4fd4da603801766b14ad8788649cfc1ad21f99a6^` = `a6a5cda5560053136897aa44301eacc6c48d8168`.
- **Fixed version/commit:** `4fd4da603801766b14ad8788649cfc1ad21f99a6`, which adds upload filename basename sanitization via `_safe_upload_filename()`.
- **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.

## Impact Parity

- **Disclosed/claimed maximum impact:** Remote code execution (`code_execution`) after unauthenticated path traversal upload.
- **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.
- **Parity:** `full`.
- **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.

## Root Cause

At the vulnerable checkout, `AttachmentService.save_upload_file()` trusts `UploadFile.filename` and joins it directly onto a temporary directory:

```python
filename = upload.filename or "upload.bin"
temp_dir = Path(tempfile.mkdtemp(prefix="mac_upload_"))
temp_path = temp_dir / filename
with temp_path.open("wb") as buffer:
    ...
```

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

The 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()`.

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

Fix commit: https://github.com/OpenBMB/ChatDev/commit/4fd4da603801766b14ad8788649cfc1ad21f99a6

## Reproduction Steps

1. Run `bundle/repro/reproduction_steps.sh` with `bash`.
2. The script:
   - Reuses the prepared project cache at `<project_cache_dir>/repo` or clones `https://github.com/OpenBMB/ChatDev.git` if absent.
   - Resolves the fixed commit and checks out `4fd4da603801766b14ad8788649cfc1ad21f99a6^` for vulnerable runs and `4fd4da603801766b14ad8788649cfc1ad21f99a6` for fixed runs.
   - Starts the original product server via `server_main.py --host 127.0.0.1 --port <port>` and waits for `/health`.
   - Opens the real `/ws` WebSocket to mint an unauthenticated session.
   - Sends a real multipart `POST /api/uploads/{session_id}` request with a traversal filename targeting a symlink under `/tmp`.
   - 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/`.
   - 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.
3. Expected evidence:
   - `bundle/repro/runtime_manifest.json` with `entrypoint_kind=api_remote`, `service_started=true`, `healthcheck_passed=true`, and `target_path_reached=true`.
   - `bundle/repro/eval_summary.json` with both vulnerable code-execution attempts true and both fixed-blocked attempts true.
   - `bundle/repro/result_vuln_1.json`, `result_vuln_2.json`, `result_fixed_1.json`, and `result_fixed_2.json`.
   - `bundle/repro/rce_marker_vuln_1.txt` and `bundle/repro/rce_marker_vuln_2.txt` containing `RCE_OK_FROM_CHATDEV_WORKFLOW ...`.
   - Runtime logs in `bundle/logs/`.

## Evidence

The script was run twice consecutively and succeeded both times. The final run produced:

```json
{
  "vuln_attempts_code_execution": [true, true],
  "vuln_attempts_persistent_yaml_write": [true, true],
  "vuln_response_names": [
    "../../../../../../../../../../../../../../../../tmp/chatdev_cve58166_vuln_1_link.yaml",
    "../../../../../../../../../../../../../../../../tmp/chatdev_cve58166_vuln_2_link.yaml"
  ],
  "vuln_marker_contents": [
    "RCE_OK_FROM_CHATDEV_WORKFLOW role=vuln attempt=1\n",
    "RCE_OK_FROM_CHATDEV_WORKFLOW role=vuln attempt=2\n"
  ],
  "fixed_attempts_blocked_chain": [true, true],
  "fixed_response_names": [
    "chatdev_cve58166_fixed_1_link.yaml",
    "chatdev_cve58166_fixed_2_link.yaml"
  ],
  "fixed_target_created": [false, false],
  "confirmed": true
}
```

Important artifact locations:

- Main proof log: `bundle/logs/reproduction_steps.log`.
- Vulnerable server logs: `bundle/logs/server_vuln_1.log`, `bundle/logs/server_vuln_2.log`.
- Vulnerable driver logs: `bundle/logs/driver_vuln_1.log`, `bundle/logs/driver_vuln_2.log`.
- Fixed server logs: `bundle/logs/server_fixed_1.log`, `bundle/logs/server_fixed_2.log`.
- Fixed driver logs: `bundle/logs/driver_fixed_1.log`, `bundle/logs/driver_fixed_2.log`.
- Structured runtime evidence: `bundle/repro/runtime_manifest.json`.
- Marker files proving attacker-controlled Python execution: `bundle/repro/rce_marker_vuln_1.txt`, `bundle/repro/rce_marker_vuln_2.txt`.

Example 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`.

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

## Recommendations / Next Steps

- 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.
- 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.
- Avoid following symlinks for temporary upload writes where possible, or create temp files with safe APIs that prevent symlink traversal.
- 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.
- Add end-to-end regression tests that send multipart filenames with `../`, absolute paths, Windows separators, symlink targets, and the RCE chain pattern demonstrated here.
- Upgrade users to a version containing commit `4fd4da603801766b14ad8788649cfc1ad21f99a6` or later.

## Additional Notes

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