github.com/rclone/rclone: serve s3 --auth-proxy: multipart upload operations are not scoped to the authenticated tenant
What fixes it
No source in this snapshot names a fixed version.
Description
Summary
rclone serve s3 --auth-proxy maps several access key IDs to different backends, one per tenant. Ordinary object operations are isolated, each resolving a per-request VFS from the authenticated context. The multipart operations are not: UploadPart, CompleteMultipartUpload and AbortMultipartUpload find the upload by upload ID alone and act on the VFS captured at create time, never re-checking the caller. A tenant who learns another tenant's in-flight (bucket, object, uploadID) can inject bytes into, finalise, or abort that upload against the victim's backend.
Details
One s3Backend, shared by all tenants, holds two process-global maps with no tenant dimension, multipartUploads and meta (cmd/serve/s3/backend.go:42,46). Ordinary handlers call b.s.getVFS(ctx); the multipart sub-operations resolve the upload by ID only:
func (b *s3Backend) loadUpload(uploadID gofakes3.UploadID) (*multipartUpload, error) {
v, ok := b.multipartUploads.Load(uploadID)
if !ok {
return nil, gofakes3.ErrNoSuchUpload
}
return v.(*multipartUpload), nil
}UploadPart, CompleteMultipartUpload and AbortMultipartUpload all start with b.loadUpload(uploadID) and never call getVFS. CreateMultipartUpload captures the initiator's VFS in up.vfs (multipart.go:184) under a random UUIDv4 upload id; completion writes through it (up.vfs.Rename(up.streamFp, up.fp), multipart.go:413), so the object lands in the initiator's backend at the key the initiator chose. That key is fixed by the victim at initiate, so this is not an arbitrary cross-tenant write, and no other tenant's at-rest content is read.
The only remaining check is vendored gofakes3 v0.0.9, whose registry is keyed by bucket name, with no tenant dimension:
func (u *uploader) getUnlocked(bucket, object string, id UploadID) (mu *multipartUpload, err error) {
bucketUps, ok := u.buckets[bucket]then bucketUps.uploads[id], with a final mu.Bucket != bucket || mu.Object != object check (uploader.go:417,422). So the exact (bucket, object, uploadID) is the whole authorization, honoured whichever tenant presents it. Discovery is asymmetric: ListMultipartUploads and ListParts call ensureBucketExists against the caller's own VFS, so listing a victim's uploads needs the attacker to own a same-named bucket; the inject, complete and abort handlers skip that check and need no bucket once the tuple is known.
PoC
Two-tenant proxy (key A to root A, key B to root B), one loopback server, raw SigV4. Positive control: A puts bkt/secret.txt; B makes its own bkt but GET returns 404 and on disk the object is only under A's root, so the keys are separate backends. Cross-tenant, all observed: B lists a same-named bkt and sees A's upload id and key; B, owning no bucket, uploads a part that ends up in A's completed object, and separately completes A's upload into A's root; B aborts A's upload, after which A's UploadPart returns NoSuchUpload; a wrong bucket or object name is refused. The shared meta map similarly leaks custom X-Amz-Meta-* cross-tenant on HeadObject.
Preconditions
One server, --auth-proxy, several keys to different backends; the attacker is an authenticated tenant. Inject, complete and abort need the victim's (bucket, object, uploadID); the id is a random UUIDv4, so it must be obtained, demonstrated here via ListMultipartUploads while owning a same-named bucket. The victim upload must be in flight; the reaper aborts idle uploads only after --multipart-expiry (default 24h).
Affected operations
Cross the boundary: UploadPart, CompleteMultipartUpload, AbortMultipartUpload (observed), plus ListMultipartUploads and ListParts (metadata). Unaffected: ordinary object operations, which resolve the VFS per request. Untested inference: with --disable-multipart-streaming and no VFS cache, gofakes3 uses its in-memory uploader and completion calls PutObject with the completing request's context, so a cross-tenant complete would write into the completer's own store; the upload-ID gate still permits cross-tenant abort. I did not run this.
Impact
Integrity and availability of in-flight multipart uploads across tenants meant to be isolated, plus disclosure of upload and custom object metadata. Not a breach of at-rest content, and the destination key is not attacker-chosen. Per-key isolation is intended: TestAuthKeyPerServer and TestAuthProxyKeysNotRegistered in cmd/serve/s3/s3_test.go assert separate keys and proxy secrets do not cross tenants. I am not proposing a CVSS vector and leave scoring to the maintainer.
Suggested fix
Scope each multipart sub-operation to the caller: bind the upload to the caller's VFS at create time and reject UploadPart, CompleteMultipartUpload, AbortMultipartUpload, ListParts and ListMultipartUploads entries belonging to another tenant, for example by comparing the request-context VFS against up.vfs.
Version
Reproduced on v1.75.1. cmd/serve/s3/multipart.go and cmd/serve/s3/backend.go are byte-identical on master at commit 1c3e432c3fdbf98c5800c0390496d6944bac53b0, and master pins the same gofakes3 v0.0.9
Text from GitHub Advisory Database
Affected versions 1
- github.com/rclone/rclone, go>=v1.68.0 <= 1.75.1GitHub