GHSA-mx42-j6wv-px98: RustFS has an authorization bypass in multipart UploadPartCopy enables cross-bucket object exfiltration
RustFS contains a missing authorization check in the multipart copy path (`UploadPartCopy`). A low-privileged user who cannot read objects from a victim bucket can still exfiltrate victim objects by copying them into an attacker-controlled multipart upload and completing the upload.This breaks tenant isolation in multi-user / multi-tenant deployments.Impact**Unauthorized cross-bucket / cross-tenant data exfiltration (Confidentiality: High).**An attacker with only minimal permissions on their own bucket (multipart upload + Put/Get on destination objects) can copy and retrieve objects from a victim bucket **without** having `s3:GetObject` (or equivalent) permission on the source.In the attached PoC, the attacker successfully exfiltrates a 5MB private object and proves integrity via matching SHA256 and size.Threat Model (Realistic)**Victim tenant/user** owns a bucket (e.g., `victim-bucket-*`) and stores private objects (e.g., `private/finance_dump.bin`).**Attacker tenant/user** has **no permissions** on the victim bucket:cannot `ListObjects`, `HeadObject`, `GetObject`, or `CopyObject` from the victim bucket.Attacker has **minimal permissions only on attacker bucket**:`CreateMultipartUpload`, `UploadPart`, `UploadPartCopy`, `CompleteMultipartUpload`, `AbortMultipartUpload`,and `PutObject`/`GetObject` for objects in attacker bucket.Despite this, attacker can exfiltrate victim objects via multipart copy.Root Cause AnalysisThe access control layer fails open for multipart copy-related operations:File: `rustfs/src/storage/access.rs``abort_multipart_upload()` returns `Ok(())` without authorization (L435–437)`complete_multipart_upload()` returns `Ok(())` without authorization (L442–444)`upload_part_copy()` returns `Ok(())` without authorization (L1446–1448)In contrast, `copy_object()` correctly enforces authorization:source `GetObject` authorization (L469)destination `PutObject` authorization (L478)The multipart copy implementation reads the source object directly:File: `rustfs/src/app/multipart_usecase.rs``store.get_object_reader(&src_bucket, &src_key, ...)` (L959–962)Because `upload_part_copy()` does not enforce source `GetObject` authorization, the server reads and copies victim data even when the requester lacks permission.Affected Versions**Tested vulnerable on:** `main` @ `c1d5106acc3480c275a52344df84633bb6dcd8f0`**Git describe:** `1.0.0-alpha.86-3-gc1d5106a`The fail-open authorization behavior for `UploadPartCopy` was introduced in:**Commit:** `09ea11c13` (per `git blame` on `rustfs/src/storage/access.rs:1443-1448`)**Affected range (recommended wording):**All versions **from** commit `09ea11c13` **through** `c1d5106acc3480c275a52344df84633bb6dcd8f0` (and likely any releases containing those commits) until a fix is applied.Package version (Cargo metadata)`rustfs` crate version in this tree: **0.0.5** (`cargo metadata`)Proof of Concept (PoC) – Real Commands + Verified ResultsFilesPlace the PoC script at the repository root:**PoC script:** [`poc_uploadpartcopy_exfil_v3.sh`](https://github.com/user-attachments/files/26006935/poc_uploadpartcopy_exfil_v3.sh)**Captured output:** [`poc_v3_output.txt`](https://github.com/user-attachments/files/26006938/poc_v3_output.txt)*(Optional)* **Redacted debug log:** `upload_part_copy_debug_redacted.log` (Authorization/signature redacted)EnvironmentRustFS running locally (Docker is simplest), listening on:`http://127.0.0.1:9000`Tools:`awscli`, `jq`, `awscurl`Steps to ReproduceStart RustFS (example):Run the PoC and save output:Attachments[`poc_uploadpartcopy_exfil_v3.sh`](https://github.com/user-attachments/files/26006950/poc_uploadpartcopy_exfil_v3.sh)[`poc_v3_output.txt`](https://github.com/user-attachments/files/26006953/poc_v3_output.txt)Expected BehaviorAttacker operations against victim bucket should be denied:`ListObjects` -> AccessDenied`HeadObject` -> AccessDenied`GetObject` -> AccessDenied`CopyObject` -> AccessDenied`UploadPartCopy` from victim -> attacker multipart should also be denied.Actual BehaviorAll direct operations against victim are denied (as expected),but **`UploadPartCopy` succeeds**, and attacker retrieves the copied object from attacker bucket.Observed PoC OutputVictim uploads a private object:size: `5,242,880` bytessha256: `fda018db1da9d8f4c1b287c75943384a3b4ede391ec156039b6d94e17d6ad68f`Attacker exfiltrates it via multipart copy:stolen size: `5,242,880` bytesstolen sha256: `fda018db1da9d8f4c1b287c75943384a3b4ede391ec156039b6d94e17d6ad68f`Proof:hashes and sizes match (victim == stolen) -> unauthorized cross-bucket read confirmed.Network Evidence (Redacted)The debug log shows a successful request with:HTTP method: `PUT`destination: `/<attacker-bucket>/<dst-key>?partNumber=1&uploadId=...`header: `x-amz-copy-source: <victim-bucket>/private/finance_dump.bin`response: `HTTP/1.1 200` with `<CopyPartResult><ETag>...</ETag>...</CopyPartResult>`FixImplement authorization checks equivalent to `copy_object()` for multipart copy paths:`upload_part_copy`:enforce **source** `GetObject` authorization on `x-amz-copy-source`enforce **destination** `PutObject` authorization on the target object(recommended) apply the same tag-condition enforcement used by `copy_object()` on the source.`complete_multipart_upload`:enforce destination `PutObject` authorization`abort_multipart_upload`:enforce appropriate multipart permission (or destination `PutObject` as a safe boundary)
CVSS v4.0: 5.3
CVSS v3.1: 4.3