{"schema":"threatline.record/2","record":{"id":"GHSA-35g6-9fjv-rmx5","aliases":["GHSA-35g6-9fjv-rmx5"],"summary":"serve s3 --auth-proxy: multipart upload operations are not scoped to the authenticated tenant","details":"### Summary\r\n\r\n`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.\r\n\r\n### Details\r\n\r\nOne `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:\r\n\r\n```go\r\nfunc (b *s3Backend) loadUpload(uploadID gofakes3.UploadID) (*multipartUpload, error) {\r\n\tv, ok := b.multipartUploads.Load(uploadID)\r\n\tif !ok {\r\n\t\treturn nil, gofakes3.ErrNoSuchUpload\r\n\t}\r\n\treturn v.(*multipartUpload), nil\r\n}\r\n```\r\n\r\n`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.\r\n\r\nThe only remaining check is vendored gofakes3 v0.0.9, whose registry is keyed by bucket name, with no tenant dimension:\r\n\r\n```go\r\nfunc (u *uploader) getUnlocked(bucket, object string, id UploadID) (mu *multipartUpload, err error) {\r\n\tbucketUps, ok := u.buckets[bucket]\r\n```\r\n\r\nthen `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.\r\n\r\n### PoC\r\n\r\nTwo-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`.\r\n\r\n### Preconditions\r\n\r\nOne 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).\r\n\r\n### Affected operations\r\n\r\nCross 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.\r\n\r\n### Impact\r\n\r\nIntegrity 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.\r\n\r\n### Suggested fix\r\n\r\nScope 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`.\r\n\r\n### Version\r\n\r\nReproduced 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 ","categories":["Unclassified"],"product":"github.com/rclone/rclone","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/rclone/rclone/security/advisories/GHSA-35g6-9fjv-rmx5","source_modified_at":"2026-10-09T10:55:33Z","fetched_at":"2026-10-25T16:01:37.004770Z","package":{"ecosystem":"go","name":"github.com/rclone/rclone"},"range":">=v1.68.0 <= 1.75.1"}],"fixed_versions":[],"cvss":6.4,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-09T10:55:33Z","modified_at":"2026-10-09T10:55:33Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/rclone/rclone/security/advisories/GHSA-35g6-9fjv-rmx5","fetched_at":"2026-10-25T16:01:37.004770Z","source_modified_at":"2026-10-09T10:55:33Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-09T10:55:33Z","content_hash":"4305506396658624121519961464babc2f967f5ff800b643a0f80c63cb0f62e5"}}
