CVE-2026-72572: Verified Reproduction
CVE-2026-72572: Unauthenticated path traversal in xmysql /download allows arbitrary file read via unsanitized req.query.name .
CVE-2026-72572 is verified against xmysql · npm / JavaScript (Node.js, Express). Affected versions: all versions (project is deprecated/superseded by nocodb; no patched version exists). Vulnerability class: Path Traversal. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00324.
What Is CVE-2026-72572?
CVE-2026-72572 is a high-severity Path Traversal vulnerability affecting xmysql all versions (project is deprecated/superseded by nocodb; no patched version exists). Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00324).
CVE-2026-72572 Severity & CVSS Score
CVE-2026-72572 is rated high severity, with a CVSS base score of 7.5 out of 10.
High — serious impact or readily exploitable. Prioritize remediation.
Affected xmysql Versions
xmysql · npm / JavaScript (Node.js, Express) versions all versions (project is deprecated/superseded by nocodb; no patched version exists) are affected.
How to Reproduce CVE-2026-72572
pruva-verify REPRO-2026-00324 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00324/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-72572
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
name query parameter of GET /download (unauthenticated)
- GET /download?name=../../../../../../../etc/passwd
- lib/xapi.js downloadFile
- path.join(process.cwd(), req.query.name)
- res.download(file)
How the agent worked
Root Cause and Exploit Chain for CVE-2026-72572
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.
- Package/component:
o1lab/xmysql(npmxmysql), route handlerlib/xapi.js:downloadFile(lines 424–427 at commit8c6b00e). - 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 = 1inlib/util/cmd.helper.js) andreadOnlyis false (the default), so default deployments are exposed.
Impact Parity
- Disclosed/claimed maximum impact: unauthenticated remote arbitrary file disclosure
(
info_leakviaapi_remote). - Reproduced impact from this run: unauthenticated remote arbitrary file read —
/etc/passwdreturned byte-identical over HTTP 200, and a per-run planted secret file (/tmp/pruva_xmysql_secret.txtwith 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:
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:
- no authentication middleware on the route (registered as
this.app.get("/download", this.downloadFile.bind(this))inside thedynamic === 1 && !readOnlyblock atlib/xapi.js:322–340), - no allowlist/confined upload-download directory, and
- no rejection of
..or absolute-path components.
- Fix commit: none known; xmysql is unmaintained/archived (renamed to NocoDB).
Reproduction Steps
bundle/repro/reproduction_steps.sh(idempotent; run twice consecutively, both exit 0).- The script:
- reuses the prepared project cache (
/pruva/project-cache/repo) or cloneshttps://github.com/o1lab/xmysql, then pins commit8c6b00ee22860230975e43ab705d015d2235e308(v0.6.0, latest master) and asserts the vulnerable line is present inlib/xapi.js; - installs and starts MariaDB, creates schema
reprodbwith 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 unusedsleep@6.1.0native 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 3000and waits for/_health; - plants a unique-token secret file outside the app working directory;
- sends the unauthenticated attacker request
GET /download?name=../../../../../../../etc/passwdandGET /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/passwdand token recovery, then writesbundle/repro/runtime_manifest.jsonand exits 0 on success.
- reuses the prepared project cache (
- Expected evidence: HTTP 200 responses with
Content-Disposition: attachmentfor both traversal requests; downloaded/etc/passwdbyte-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—/_healthresponse proving service liveness.bundle/logs/download_passwd_headers.txt+bundle/logs/downloaded_passwd.txt— HTTP 200 and byte-identical/etc/passwd(diffclean,^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 digestd7544f1501df408c3c5353b430cef5842b463250b64824974436575afb948112).
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 withSTORAGE_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 thedynamicblock (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 tolocalhost(may resolve to::1); the script probes both127.0.0.1andlocalhost.
CVE-2026-72572 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
ls -la /pruva/project-cache/ 2>/dev/null; ls /pruva/project-cache/repo-mirrors 2>/dev/null; node --version; npm --version; which mysqld mariadbd 2>/dev/null; echo "---"; df -h /pruva 2>/dev/null | tail -1total 0 drwxr-xr-x 4 vscode vscode 80 Aug 10 14:57 . drwxr-xr-x 1 root root 26 Aug 10 14:57 .. drwxr-xr-x 2 vscode vscode 60 Aug 10 14:57 .pruva drwxr-xr-x 2 vscode vscode 40 Aug 10 14:57 repo-mirrors v24.18.0 11.16.0 ---
git clone --depth 50 https://github.com/o1lab/xmysql /pruva/project-cache/repo 2>&1 | tail -3; ls /pruva/project-cache/repoCloning into '/pruva/project-cache/repo'... CONTRIBUTING.md ISSUE_TEMPLATE.md LICENSE README.md assets bin dev.Dockerfile
apt-cache policy mariadb-server mysql-server 2>/dev/null | head -20; ls /etc/mysql 2>/dev/null; which docker/usr/bin/docker
grep -n "download" /pruva/project-cache/repo/lib/xapi.js | head; grep -n "dynamic\|readOnly" /pruva/project-cache/repo/lib/xapi.js | head -20338: this.app.get("/download", this.downloadFile.bind(this));
424: downloadFile(req, res) {
426: res.download(file);
156: if (!this.config.readOnly)
171: if (!this.config.readOnly) {
183: if (!this.config.readOnly) {
195: if (!this.config.readOnly) {
209: if (!this.config.readOnly) {docker ps 2>&1 | head -3; docker images 2>&1 | head -10CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES WARNING: This output is designed for human readability. For machine-readable output, please use --format. IMAGE ID DISK USAGE CONTENT SIZE EXTRA pruva/rootless-readiness:docker-27 0aec26e1b423 1.47MB 0B
sudo apt-get update -qq 2>&1 | tail -2; apt-cache policy mariadb-server 2>&1 | head -5mariadb-server:
Installed: (none)
Candidate: 1:11.8.6-5ubuntu0.1
Version table:
1:11.8.6-5ubuntu0.1 500
grep -n "setupRoutesForEachTable\|dynamic\|init\|connect" /pruva/project-cache/repo/lib/xapi.js | head -20; head -60 /pruva/project-cache/repo/index.js35: init(cbk) {
36: this.mysql.init((err, results) => {
322: if (this.config.dynamic === 1 && !this.config.readOnly) {
324: .route("/dynamic*")
exports.xapi = require('./lib/xapi.js')
Artifacts and Evidence for CVE-2026-72572
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-72572
FAQ: CVE-2026-72572
Is CVE-2026-72572 exploitable?
How severe is CVE-2026-72572?
What type of vulnerability is CVE-2026-72572?
Which versions of xmysql are affected by CVE-2026-72572?
How can I reproduce CVE-2026-72572?
Is the CVE-2026-72572 reproduction verified?
References for CVE-2026-72572
Authoritative sources for CVE-2026-72572 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.