# REPRO-2026-00324: Unauthenticated path traversal in xmysql `/download` allows arbitrary file read via unsanitized `req.query.name`. ## Summary Status: published Severity: high CVSS: 7.5 / 10 CWE: CWE-22 (Path Traversal) (Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00324 CVE: CVE-2026-72572 ## Package Name: xmysql Ecosystem: npm / JavaScript (Node.js, Express) Affected: all versions (project is deprecated/superseded by nocodb; no patched version exists) Fixed: Unknown ## Root Cause # Root Cause Analysis — CVE-2026-72572: xmysql `/download` Path Traversal ## Summary xmysql (o1lab/xmysql, a zero-config REST API generator for MySQL/MariaDB) exposes an unauthenticated `GET /download` endpoint whose handler `downloadFile(req, res)` in `lib/xapi.js` builds a filesystem path with `path.join(process.cwd(), req.query.name)` and passes it directly to Express `res.download(file)`. The `name` query parameter is never validated, normalized against, or confined to a base directory, so an unauthenticated remote attacker can supply `../` traversal sequences and read any file readable by the xmysql process. ## Impact - **Package/component:** `o1lab/xmysql` (npm `xmysql`), route handler `lib/xapi.js:downloadFile` (lines 424–427 at commit `8c6b00e`). - **Affected versions:** all versions. The project was renamed to NocoDB and the repository archived; no patched upstream version of xmysql exists. - **Risk level and consequences:** High. Unauthenticated arbitrary file read from the server filesystem (database credentials, `/etc/passwd`, TLS keys, application source, cloud metadata files on disk, etc.). The route is registered by default whenever the MySQL host is localhost (`program.dynamic = 1` in `lib/util/cmd.helper.js`) and `readOnly` is false (the default), so default deployments are exposed. ## Impact Parity - **Disclosed/claimed maximum impact:** unauthenticated remote arbitrary file disclosure (`info_leak` via `api_remote`). - **Reproduced impact from this run:** unauthenticated remote arbitrary file read — `/etc/passwd` returned byte-identical over HTTP 200, and a per-run planted secret file (`/tmp/pruva_xmysql_secret.txt` with a unique token) was recovered verbatim through the same endpoint. - **Parity:** `full`. - **Not demonstrated:** nothing claimed beyond file disclosure; no further impact was claimed or required. ## Root Cause In `lib/xapi.js`: ```js downloadFile(req, res) { let file = path.join(process.cwd(), req.query.name); res.download(file); } ``` `req.query.name` is fully attacker-controlled. `path.join` resolves `..` segments lexically, so a value such as `../../../../../../etc/passwd` escapes the process working directory entirely (excess `..` above `/` collapse to `/`). The result is handed to `res.download()`, which happily streams any file the process can read. There is: 1. no authentication middleware on the route (registered as `this.app.get("/download", this.downloadFile.bind(this))` inside the `dynamic === 1 && !readOnly` block at `lib/xapi.js:322–340`), 2. no allowlist/confined upload-download directory, and 3. no rejection of `..` or absolute-path components. - **Fix commit:** none known; xmysql is unmaintained/archived (renamed to NocoDB). ## Reproduction Steps 1. `bundle/repro/reproduction_steps.sh` (idempotent; run twice consecutively, both exit 0). 2. The script: - reuses the prepared project cache (`/pruva/project-cache/repo`) or clones `https://github.com/o1lab/xmysql`, then pins commit `8c6b00ee22860230975e43ab705d015d2235e308` (v0.6.0, latest master) and asserts the vulnerable line is present in `lib/xapi.js`; - installs and starts MariaDB, creates schema `reprodb` with a table, and gives the TCP account a native password (dual-mode SQL runner keeps it idempotent); - installs node dependencies with `npm install --ignore-scripts` (the declared but unused `sleep@6.1.0` native module fails to build on modern Node and is irrelevant); - starts the real product: `node bin/index.js -h 127.0.0.1 -u root -p rootpass -d reprodb -n 3000` and waits for `/_health`; - plants a unique-token secret file outside the app working directory; - sends the unauthenticated attacker request `GET /download?name=../../../../../../../etc/passwd` and `GET /download?name=../../../../../../../tmp/pruva_xmysql_secret.txt`; - runs a benign control request (`name=definitely_not_here.txt`, observed HTTP 400); - verifies byte-identity with `/etc/passwd` and token recovery, then writes `bundle/repro/runtime_manifest.json` and exits 0 on success. 3. **Expected evidence:** HTTP 200 responses with `Content-Disposition: attachment` for both traversal requests; downloaded `/etc/passwd` byte-identical to the real file; planted secret token recovered. ## Evidence - `bundle/logs/reproduction_steps.log` — full session log of both runs. - `bundle/logs/xmysql_service.log` — real xmysql startup banner ("REST APIs Generated: 135"). - `bundle/logs/health_response.json` — `/_health` response proving service liveness. - `bundle/logs/download_passwd_headers.txt` + `bundle/logs/downloaded_passwd.txt` — HTTP 200 and byte-identical `/etc/passwd` (`diff` clean, `^root:` present). - `bundle/logs/download_secret_headers.txt` + `bundle/logs/downloaded_secret.txt` — unique per-run token (e.g. `PRUVA_XMYSQL_SECRET_1786374155853866884`) recovered via traversal. - `bundle/logs/download_benign_status.txt` — benign control returned HTTP 400. - Environment: Linux x86_64, Node.js v24.18.0, MariaDB 11.8.6, xmysql v0.6.0 @ `8c6b00ee22860230975e43ab705d015d2235e308` (target digest `d7544f1501df408c3c5353b430cef5842b463250b64824974436575afb948112`). ## Recommendations / Next Steps - **Fix approach:** drop the endpoint or confine downloads to a dedicated storage directory: resolve `path.resolve(STORAGE_DIR, name)` and reject any result that does not start with `STORAGE_DIR + path.sep`; additionally reject `..`/absolute inputs and require authentication/authorization on the route. - **Upgrade guidance:** xmysql is unmaintained; migrate to NocoDB or another maintained API layer. Until then, run with `--readOnly` (or a non-localhost DB host) so the `dynamic` block (upload/download routes) is never registered, or front the service with a proxy that blocks `/download`. - **Testing recommendations:** regression test that `GET /download?name=../...` returns 4xx and that downloads are confined to the storage directory. ## Additional Notes - **Idempotency:** the script was executed twice consecutively in the same workspace and both runs exited 0; DB setup is dual-mode (unix-socket root first run, TCP password on re-runs) and service startup kills any previous instance. - **Negative control:** no patched upstream version exists (all versions affected, repository archived), so a fixed-version differential is not applicable; a benign in-cwd control request is included instead (HTTP 400 for a nonexistent file). - **Edge cases:** the route only exists when `dynamic === 1 && !readOnly`; dynamic defaults to 1 whenever the MySQL host is localhost/127.0.0.1/::1, which is the common deployment. The server binds to `localhost` (may resolve to `::1`); the script probes both `127.0.0.1` and `localhost`. ## Reproduction Details Reproduced: 2026-08-23T15:38:18.998Z Duration: 731 seconds Tool calls: 126 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00324 pruva-verify CVE-2026-72572 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00324&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00324/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-72572 - Source: https://nvd.nist.gov/vuln/detail/CVE-2026-72572 ## Artifacts - bundle/repro/rca_report.md (analysis, 6862 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 9502 bytes) - bundle/logs/download_benign_status.txt (other, 3 bytes) - bundle/logs/download_passwd_headers.txt (other, 401 bytes) - bundle/logs/download_secret_headers.txt (other, 416 bytes) - bundle/logs/downloaded_passwd.txt (other, 1262 bytes) - bundle/logs/downloaded_secret.txt (other, 40 bytes) - bundle/logs/health_response.json (other, 51 bytes) - bundle/logs/xmysql_service.log (log, 1093 bytes) - bundle/repro/runtime_manifest.json (other, 1108 bytes) - bundle/repro/validation_verdict.json (other, 743 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00324 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00324/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00324 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev