GHSA-MX42-J6WV-PX98

Vulnerability from github – Published: 2026-04-08 00:15 – Updated: 2026-04-10 21:33
VLAI?
Summary
RustFS has an authorization bypass in multipart UploadPartCopy enables cross-bucket object exfiltration
Details

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 Analysis

The 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 Results

Files

Place the PoC script at the repository root:

Environment

RustFS running locally (Docker is simplest), listening on:

  • http://127.0.0.1:9000

Tools: - awscli, jq, awscurl

Steps to Reproduce

1) Start RustFS (example):

docker compose -f docker-compose-simple.yml up -d
````

2. Run the PoC and save output:

```bash
chmod +x poc_uploadpartcopy_exfil_v3.sh
./poc_uploadpartcopy_exfil_v3.sh | tee poc_v3_output.txt

Attachments

Expected Behavior

  • Attacker 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 Behavior

  • All direct operations against victim are denied (as expected),
  • but UploadPartCopy succeeds, and attacker retrieves the copied object from attacker bucket.

Observed PoC Output

Victim uploads a private object:

  • size: 5,242,880 bytes
  • sha256: fda018db1da9d8f4c1b287c75943384a3b4ede391ec156039b6d94e17d6ad68f

Attacker exfiltrates it via multipart copy:

  • stolen size: 5,242,880 bytes
  • stolen 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>

Fix

Implement 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)

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "crates.io",
        "name": "rustfs"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "0.0.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-39360"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-862"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-08T00:15:50Z",
    "nvd_published_at": "2026-04-07T19:16:46Z",
    "severity": "MODERATE"
  },
  "details": "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.\n\nThis breaks tenant isolation in multi-user / multi-tenant deployments.\n\n## Impact\n**Unauthorized cross-bucket / cross-tenant data exfiltration (Confidentiality: High).**\n\nAn 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.\n\nIn the attached PoC, the attacker successfully exfiltrates a 5MB private object and proves integrity via matching SHA256 and size.\n\n## Threat Model (Realistic)\n- **Victim tenant/user** owns a bucket (e.g., `victim-bucket-*`) and stores private objects (e.g., `private/finance_dump.bin`).\n- **Attacker tenant/user** has **no permissions** on the victim bucket:\n  - cannot `ListObjects`, `HeadObject`, `GetObject`, or `CopyObject` from the victim bucket.\n- Attacker has **minimal permissions only on attacker bucket**:\n  - `CreateMultipartUpload`, `UploadPart`, `UploadPartCopy`, `CompleteMultipartUpload`, `AbortMultipartUpload`,\n  - and `PutObject`/`GetObject` for objects in attacker bucket.\n- Despite this, attacker can exfiltrate victim objects via multipart copy.\n\n## Root Cause Analysis\nThe access control layer fails open for multipart copy-related operations:\n\nFile: `rustfs/src/storage/access.rs`\n- `abort_multipart_upload()` returns `Ok(())` without authorization (L435\u2013437)\n- `complete_multipart_upload()` returns `Ok(())` without authorization (L442\u2013444)\n- `upload_part_copy()` returns `Ok(())` without authorization (L1446\u20131448)\n\nIn contrast, `copy_object()` correctly enforces authorization:\n- source `GetObject` authorization (L469)\n- destination `PutObject` authorization (L478)\n\nThe multipart copy implementation reads the source object directly:\n\nFile: `rustfs/src/app/multipart_usecase.rs`\n- `store.get_object_reader(\u0026src_bucket, \u0026src_key, ...)` (L959\u2013962)\n\nBecause `upload_part_copy()` does not enforce source `GetObject` authorization, the server reads and copies victim data even when the requester lacks permission.\n\n## Affected Versions\n- **Tested vulnerable on:** `main` @ `c1d5106acc3480c275a52344df84633bb6dcd8f0`\n- **Git describe:** `1.0.0-alpha.86-3-gc1d5106a`\n\nThe fail-open authorization behavior for `UploadPartCopy` was introduced in:\n- **Commit:** `09ea11c13` (per `git blame` on `rustfs/src/storage/access.rs:1443-1448`)\n\n**Affected range (recommended wording):**\n- All versions **from** commit `09ea11c13` **through** `c1d5106acc3480c275a52344df84633bb6dcd8f0` (and likely any releases containing those commits) until a fix is applied.\n\n### Package version (Cargo metadata)\n- `rustfs` crate version in this tree: **0.0.5** (`cargo metadata`)\n\n## Proof of Concept (PoC) \u2013 Real Commands + Verified Results\n\n### Files\nPlace the PoC script at the repository root:\n\n- **PoC script:** [`poc_uploadpartcopy_exfil_v3.sh`](https://github.com/user-attachments/files/26006935/poc_uploadpartcopy_exfil_v3.sh)\n- **Captured output:** [`poc_v3_output.txt`](https://github.com/user-attachments/files/26006938/poc_v3_output.txt)\n- *(Optional)* **Redacted debug log:** `upload_part_copy_debug_redacted.log` (Authorization/signature redacted)\n\n### Environment \nRustFS running locally (Docker is simplest), listening on:\n\n- `http://127.0.0.1:9000`\n\nTools:\n- `awscli`, `jq`, `awscurl`\n\n### Steps to Reproduce\n1) Start RustFS (example):\n\n```bash\ndocker compose -f docker-compose-simple.yml up -d\n````\n\n2. Run the PoC and save output:\n\n```bash\nchmod +x poc_uploadpartcopy_exfil_v3.sh\n./poc_uploadpartcopy_exfil_v3.sh | tee poc_v3_output.txt\n```\n\n### Attachments\n\n* [`poc_uploadpartcopy_exfil_v3.sh`](https://github.com/user-attachments/files/26006950/poc_uploadpartcopy_exfil_v3.sh)\n* [`poc_v3_output.txt`](https://github.com/user-attachments/files/26006953/poc_v3_output.txt)\n\n\n### Expected Behavior\n\n* Attacker operations against victim bucket should be denied:\n\n  * `ListObjects` -\u003e AccessDenied\n  * `HeadObject` -\u003e AccessDenied\n  * `GetObject` -\u003e AccessDenied\n  * `CopyObject` -\u003e AccessDenied\n* `UploadPartCopy` from victim -\u003e attacker multipart should also be denied.\n\n### Actual Behavior\n\n* All direct operations against victim are denied (as expected),\n* but **`UploadPartCopy` succeeds**, and attacker retrieves the copied object from attacker bucket.\n\n### Observed PoC Output \n\nVictim uploads a private object:\n\n* size: `5,242,880` bytes\n* sha256: `fda018db1da9d8f4c1b287c75943384a3b4ede391ec156039b6d94e17d6ad68f`\n\nAttacker exfiltrates it via multipart copy:\n\n* stolen size: `5,242,880` bytes\n* stolen sha256: `fda018db1da9d8f4c1b287c75943384a3b4ede391ec156039b6d94e17d6ad68f`\n\nProof:\n\n* hashes and sizes match (victim == stolen) -\u003e unauthorized cross-bucket read confirmed.\n\n## Network Evidence (Redacted)\n\nThe debug log shows a successful request with:\n\n* HTTP method: `PUT`\n* destination: `/\u003cattacker-bucket\u003e/\u003cdst-key\u003e?partNumber=1\u0026uploadId=...`\n* header: `x-amz-copy-source: \u003cvictim-bucket\u003e/private/finance_dump.bin`\n* response: `HTTP/1.1 200` with `\u003cCopyPartResult\u003e\u003cETag\u003e...\u003c/ETag\u003e...\u003c/CopyPartResult\u003e`\n\n\n## Fix\n\nImplement authorization checks equivalent to `copy_object()` for multipart copy paths:\n\n* `upload_part_copy`:\n\n  * enforce **source** `GetObject` authorization on `x-amz-copy-source`\n  * enforce **destination** `PutObject` authorization on the target object\n  * (recommended) apply the same tag-condition enforcement used by `copy_object()` on the source.\n\n* `complete_multipart_upload`:\n\n  * enforce destination `PutObject` authorization\n\n* `abort_multipart_upload`:\n\n  * enforce appropriate multipart permission (or destination `PutObject` as a safe boundary)",
  "id": "GHSA-mx42-j6wv-px98",
  "modified": "2026-04-10T21:33:31Z",
  "published": "2026-04-08T00:15:50Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/rustfs/rustfs/security/advisories/GHSA-mx42-j6wv-px98"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39360"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/rustfs/rustfs"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "RustFS has an authorization bypass in multipart UploadPartCopy enables cross-bucket object exfiltration"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…